diff --git a/src/Docker.DotNet/Docker.DotNet.csproj b/src/Docker.DotNet/Docker.DotNet.csproj
index 205a71fea..dcf0a8bf6 100644
--- a/src/Docker.DotNet/Docker.DotNet.csproj
+++ b/src/Docker.DotNet/Docker.DotNet.csproj
@@ -7,5 +7,6 @@
+
\ No newline at end of file
diff --git a/src/Docker.DotNet/DockerClient.cs b/src/Docker.DotNet/DockerClient.cs
index b5b466724..30406dd93 100644
--- a/src/Docker.DotNet/DockerClient.cs
+++ b/src/Docker.DotNet/DockerClient.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipes;
@@ -346,22 +346,22 @@ private async Task PrivateMakeRequestAsync(
IRequestContent data,
CancellationToken cancellationToken)
{
- // If there is a timeout, we turn it into a cancellation token. At the same time, we need to link to the caller's
- // cancellation token. To avoid leaking objects, we must then also dispose of the CancellationTokenSource. To keep
- // code flow simple, we treat it as re-entering the same method with a different CancellationToken and no timeout.
+ var request = PrepareRequest(method, path, queryString, headers, data);
+
if (timeout != s_InfiniteTimeout)
{
using (var timeoutTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
{
timeoutTokenSource.CancelAfter(timeout);
-
- // We must await here because we need to dispose of the CTS only after the work has been completed.
- return await PrivateMakeRequestAsync(s_InfiniteTimeout, completionOption, method, path, queryString, headers, data, timeoutTokenSource.Token).ConfigureAwait(false);
+ return await _client.SendAsync(request, completionOption, timeoutTokenSource.Token).ConfigureAwait(false);
}
}
- var request = PrepareRequest(method, path, queryString, headers, data);
- return await _client.SendAsync(request, completionOption, cancellationToken).ConfigureAwait(false);
+ var tcs = new TaskCompletionSource();
+ using (cancellationToken.Register(() => tcs.SetCanceled()))
+ {
+ return await await Task.WhenAny(tcs.Task, _client.SendAsync(request, completionOption, cancellationToken)).ConfigureAwait(false);
+ }
}
private async Task HandleIfErrorResponseAsync(HttpStatusCode statusCode, HttpResponseMessage response, IEnumerable handlers)
@@ -452,4 +452,4 @@ public void Dispose()
}
internal delegate void ApiResponseErrorHandlingDelegate(HttpStatusCode statusCode, string responseBody);
-}
\ No newline at end of file
+}
diff --git a/src/Docker.DotNet/Endpoints/StreamUtil.cs b/src/Docker.DotNet/Endpoints/StreamUtil.cs
index 4d6704583..03e1c1b62 100644
--- a/src/Docker.DotNet/Endpoints/StreamUtil.cs
+++ b/src/Docker.DotNet/Endpoints/StreamUtil.cs
@@ -1,97 +1,55 @@
-using System;
-using System.IO;
-using System.Net.Http;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using Newtonsoft.Json;
-
-namespace Docker.DotNet.Models
-{
- internal static class StreamUtil
- {
- private static Newtonsoft.Json.JsonSerializer _serializer = new Newtonsoft.Json.JsonSerializer();
-
- internal static async Task MonitorStreamAsync(Task streamTask, DockerClient client, CancellationToken cancel, IProgress progress)
- {
- using (var stream = await streamTask)
- {
- // ReadLineAsync must be cancelled by closing the whole stream.
- using (cancel.Register(() => stream.Dispose()))
- {
- using (var reader = new StreamReader(stream, new UTF8Encoding(false)))
- {
- string line;
- while ((line = await reader.ReadLineAsync()) != null)
- {
- progress.Report(line);
- }
- }
- }
- }
- }
-
- internal static async Task MonitorStreamForMessagesAsync(Task streamTask, DockerClient client, CancellationToken cancel, IProgress progress)
- {
- using (var stream = await streamTask)
- using (var reader = new StreamReader(stream, new UTF8Encoding(false)))
- using (var jsonReader = new JsonTextReader(reader) { SupportMultipleContent = true })
- {
- while (await jsonReader.ReadAsync().WithCancellation(cancel))
- {
- var ev = _serializer.Deserialize(jsonReader);
- progress?.Report(ev);
- }
- }
- }
-
- internal static async Task MonitorResponseForMessagesAsync(Task responseTask, DockerClient client, CancellationToken cancel, IProgress progress)
- {
- using (var response = await responseTask)
- {
- await client.HandleIfErrorResponseAsync(response.StatusCode, response);
-
- using (var stream = await response.Content.ReadAsStreamAsync())
- {
- // ReadLineAsync must be cancelled by closing the whole stream.
- using (cancel.Register(() => stream.Dispose()))
- {
- using (var reader = new StreamReader(stream, new UTF8Encoding(false)))
- {
- string line;
- try
- {
- while ((line = await reader.ReadLineAsync()) != null)
- {
- var prog = client.JsonSerializer.DeserializeObject(line);
- if (prog == null) continue;
-
- progress.Report(prog);
- }
- }
- catch (ObjectDisposedException)
- {
- // The subsequent call to reader.ReadLineAsync() after cancellation
- // will fail because we disposed the stream. Just ignore here.
- }
- }
- }
- }
- }
- }
-
- private static async Task WithCancellation(this Task task, CancellationToken cancellationToken)
- {
- var tcs = new TaskCompletionSource();
- using (cancellationToken.Register(s => ((TaskCompletionSource)s).TrySetResult(true), tcs))
- {
- if (task != await Task.WhenAny(task, tcs.Task))
- {
- throw new OperationCanceledException(cancellationToken);
- }
- }
-
- return await task;
- }
- }
-}
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Net.Http;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+
+namespace Docker.DotNet.Models
+{
+ internal static class StreamUtil
+ {
+ internal static async Task MonitorStreamAsync(Task streamTask, DockerClient client, CancellationToken cancellationToken, IProgress progress)
+ {
+ var tcs = new TaskCompletionSource();
+
+ using (var stream = await streamTask)
+ using (var reader = new StreamReader(stream, new UTF8Encoding(false)))
+ using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
+ {
+ string line;
+ while ((line = await await Task.WhenAny(reader.ReadLineAsync(), tcs.Task)) != null)
+ {
+ progress.Report(line);
+ }
+ }
+ }
+
+ internal static async Task MonitorStreamForMessagesAsync(Task streamTask, DockerClient client, CancellationToken cancellationToken, IProgress progress)
+ {
+ var tcs = new TaskCompletionSource();
+
+ using (var stream = await streamTask)
+ using (var reader = new StreamReader(stream, new UTF8Encoding(false)))
+ using (var jsonReader = new JsonTextReader(reader) { SupportMultipleContent = true })
+ using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
+ {
+ while (await await Task.WhenAny(jsonReader.ReadAsync(cancellationToken), tcs.Task))
+ {
+ var ev = await client.JsonSerializer.Deserialize(jsonReader, cancellationToken);
+ progress.Report(ev);
+ }
+ }
+ }
+
+ internal static async Task MonitorResponseForMessagesAsync(Task responseTask, DockerClient client, CancellationToken cancel, IProgress progress)
+ {
+ using (var response = await responseTask)
+ {
+ await MonitorStreamForMessagesAsync(response.Content.ReadAsStreamAsync(), client, cancel, progress);
+ }
+ }
+ }
+}
diff --git a/src/Docker.DotNet/JsonSerializer.cs b/src/Docker.DotNet/JsonSerializer.cs
index 297d829ca..748c6b478 100644
--- a/src/Docker.DotNet/JsonSerializer.cs
+++ b/src/Docker.DotNet/JsonSerializer.cs
@@ -1,4 +1,6 @@
-using Newtonsoft.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace Docker.DotNet
@@ -8,6 +10,8 @@ namespace Docker.DotNet
///
internal class JsonSerializer
{
+ private readonly Newtonsoft.Json.JsonSerializer _serializer;
+
private readonly JsonSerializerSettings _settings = new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
@@ -24,6 +28,23 @@ internal class JsonSerializer
public JsonSerializer()
{
+ _serializer = Newtonsoft.Json.JsonSerializer.CreateDefault(this._settings);
+ }
+
+ public Task Deserialize(JsonReader jsonReader, CancellationToken cancellationToken)
+ {
+ var tcs = new TaskCompletionSource();
+ using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
+ {
+ Task.Factory.StartNew(
+ () => tcs.TrySetResult(_serializer.Deserialize(jsonReader)),
+ cancellationToken,
+ TaskCreationOptions.LongRunning,
+ TaskScheduler.Default
+ );
+
+ return tcs.Task;
+ }
}
public T DeserializeObject(string json)
@@ -36,4 +57,4 @@ public string SerializeObject(T value)
return JsonConvert.SerializeObject(value, this._settings);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/ChunkedReadStream.cs b/src/Docker.DotNet/Microsoft.Net.Http.Client/ChunkedReadStream.cs
index 1c5fc43a1..10768045e 100644
--- a/src/Docker.DotNet/Microsoft.Net.Http.Client/ChunkedReadStream.cs
+++ b/src/Docker.DotNet/Microsoft.Net.Http.Client/ChunkedReadStream.cs
@@ -79,7 +79,7 @@ public override int WriteTimeout
public override int Read(byte[] buffer, int offset, int count)
{
- return ReadAsync(buffer, offset, count, CancellationToken.None).Result;
+ return ReadAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
}
public async override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
diff --git a/test/Docker.DotNet.Tests/IContainerOperationsTests.cs b/test/Docker.DotNet.Tests/IContainerOperationsTests.cs
new file mode 100644
index 000000000..d94089545
--- /dev/null
+++ b/test/Docker.DotNet.Tests/IContainerOperationsTests.cs
@@ -0,0 +1,812 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using Docker.DotNet.Models;
+using Newtonsoft.Json;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Docker.DotNet.Tests
+{
+ [Collection("Test collection")]
+ public class IContainerOperationsTests
+ {
+ private readonly CancellationTokenSource _cts;
+ private readonly DockerClient _dockerClient;
+ private readonly DockerClientConfiguration _dockerClientConfiguration;
+ private readonly string _imageId;
+ private readonly Tests.TestOutput _output;
+
+ public IContainerOperationsTests(TestFixture testFixture, ITestOutputHelper outputHelper)
+ {
+ // Do not wait forever in case it gets stuck
+ _cts = CancellationTokenSource.CreateLinkedTokenSource(testFixture.cts.Token);
+ _cts.CancelAfter(TimeSpan.FromMinutes(5));
+ _cts.Token.Register(() => throw new TimeoutException("IContainerOperationsTest timeout"));
+
+ _dockerClient = testFixture.dockerClient;
+ _dockerClientConfiguration = testFixture.dockerClientConfiguration;
+ _output = new TestOutput(outputHelper);
+ _imageId = testFixture.imageId;
+ }
+
+ [Fact]
+ public async Task CreateContainerAsync_CreatesContainer()
+ {
+ var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync(
+ new CreateContainerParameters
+ {
+ Image = _imageId,
+ Name = Guid.NewGuid().ToString(),
+ },
+ _cts.Token
+ );
+
+ Assert.NotNull(createContainerResponse);
+ Assert.NotEmpty(createContainerResponse.ID);
+ }
+
+ // Timeout causing task to be cancelled
+ [Theory]
+ [InlineData(1)]
+ [InlineData(5)]
+ [InlineData(10)]
+ public async Task CreateContainerAsync_TimeoutExpires_Fails(int millisecondsTimeout)
+ {
+ using var dockerClientWithTimeout = _dockerClientConfiguration.CreateClient();
+
+ dockerClientWithTimeout.DefaultTimeout = TimeSpan.FromMilliseconds(millisecondsTimeout);
+
+ _output.WriteLine($"Time available for CreateContainer operation: {millisecondsTimeout} ms'");
+
+ var timer = new Stopwatch();
+ timer.Start();
+
+ var createContainerTask = dockerClientWithTimeout.Containers.CreateContainerAsync(
+ new CreateContainerParameters
+ {
+ Image = _imageId,
+ Name = Guid.NewGuid().ToString(),
+ },
+ _cts.Token);
+
+ _ = await Assert.ThrowsAsync(() => createContainerTask);
+
+ timer.Stop();
+ _output.WriteLine($"CreateContainerOperation finished after {timer.ElapsedMilliseconds} ms");
+
+ Assert.True(createContainerTask.IsCanceled);
+ Assert.True(createContainerTask.IsCompleted);
+ }
+
+ [Fact]
+ public async Task GetContainerLogs_Follow_False_TaskIsCompleted()
+ {
+ using var containerLogsCts = new CancellationTokenSource(TimeSpan.FromSeconds(60));
+ var logList = new List();
+
+ var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync(
+ new CreateContainerParameters()
+ {
+ Image = _imageId,
+ Name = Guid.NewGuid().ToString(),
+ Tty = false
+ },
+ _cts.Token
+ );
+
+ await _dockerClient.Containers.StartContainerAsync(
+ createContainerResponse.ID,
+ new ContainerStartParameters(),
+ _cts.Token
+ );
+
+ containerLogsCts.CancelAfter(TimeSpan.FromSeconds(20));
+
+ var containerLogsTask = _dockerClient.Containers.GetContainerLogsAsync(
+ createContainerResponse.ID,
+ new ContainerLogsParameters
+ {
+ ShowStderr = true,
+ ShowStdout = true,
+ Timestamps = true,
+ Follow = true
+ },
+ containerLogsCts.Token,
+ new Progress((m) => { _output.WriteLine(m); logList.Add(m); })
+ );
+
+ await _dockerClient.Containers.StopContainerAsync(
+ createContainerResponse.ID,
+ new ContainerStopParameters(),
+ _cts.Token
+ );
+
+ await containerLogsTask;
+ Assert.True(containerLogsTask.IsCompletedSuccessfully);
+ }
+
+ [Fact]
+ public async Task GetContainerLogs_Tty_False_Follow_False_ReadsLogs()
+ {
+ using var containerLogsCts = new CancellationTokenSource(TimeSpan.FromSeconds(50));
+ var logList = new List();
+
+ var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync(
+ new CreateContainerParameters()
+ {
+ Image = _imageId,
+ Name = Guid.NewGuid().ToString(),
+ Tty = false
+ },
+ _cts.Token
+ );
+
+ await _dockerClient.Containers.StartContainerAsync(
+ createContainerResponse.ID,
+ new ContainerStartParameters(),
+ _cts.Token
+ );
+
+ await _dockerClient.Containers.GetContainerLogsAsync(
+ createContainerResponse.ID,
+ new ContainerLogsParameters
+ {
+ ShowStderr = true,
+ ShowStdout = true,
+ Timestamps = true,
+ Follow = false
+ },
+ containerLogsCts.Token,
+ new Progress((m) => { logList.Add(m); _output.WriteLine(m); })
+ );
+
+ await _dockerClient.Containers.StopContainerAsync(
+ createContainerResponse.ID,
+ new ContainerStopParameters(),
+ _cts.Token
+ );
+
+ _output.WriteLine($"Line count: {logList.Count}");
+
+ Assert.NotEmpty(logList);
+ }
+
+ [Fact]
+ public async Task GetContainerLogs_Tty_False_Follow_True_Requires_Task_To_Be_Cancelled()
+ {
+ using var containerLogsCts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token);
+
+ var logList = new List();
+
+ var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync(
+ new CreateContainerParameters()
+ {
+ Image = _imageId,
+ Name = Guid.NewGuid().ToString(),
+ Tty = false
+ },
+ _cts.Token
+ );
+
+ await _dockerClient.Containers.StartContainerAsync(
+ createContainerResponse.ID,
+ new ContainerStartParameters(),
+ _cts.Token
+ );
+
+ containerLogsCts.CancelAfter(TimeSpan.FromSeconds(5));
+
+ // Will be cancelled after CancellationTokenSource interval, would run forever otherwise
+ await Assert.ThrowsAsync(() => _dockerClient.Containers.GetContainerLogsAsync(
+ createContainerResponse.ID,
+ new ContainerLogsParameters
+ {
+ ShowStderr = true,
+ ShowStdout = true,
+ Timestamps = true,
+ Follow = true
+ },
+ containerLogsCts.Token,
+ new Progress((m) => { _output.WriteLine(JsonConvert.SerializeObject(m)); logList.Add(m); })
+ ));
+ }
+
+ [Fact]
+ public async Task GetContainerLogs_Tty_True_Follow_True_Requires_Task_To_Be_Cancelled()
+ {
+ using var containerLogsCts = new CancellationTokenSource(TimeSpan.FromSeconds(60));
+ var logList = new List();
+
+ var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync(
+ new CreateContainerParameters()
+ {
+ Image = _imageId,
+ Name = Guid.NewGuid().ToString(),
+ Tty = true
+ },
+ _cts.Token
+ );
+
+ await _dockerClient.Containers.StartContainerAsync(
+ createContainerResponse.ID,
+ new ContainerStartParameters(),
+ _cts.Token
+ );
+
+ containerLogsCts.CancelAfter(TimeSpan.FromSeconds(10));
+
+ var containerLogsTask = _dockerClient.Containers.GetContainerLogsAsync(
+ createContainerResponse.ID,
+ new ContainerLogsParameters
+ {
+ ShowStderr = true,
+ ShowStdout = true,
+ Timestamps = true,
+ Follow = true
+ },
+ containerLogsCts.Token,
+ new Progress((m) => { _output.WriteLine(m); logList.Add(m); })
+ );
+
+ await Assert.ThrowsAsync(() => containerLogsTask);
+ }
+
+ [Fact]
+ public async Task GetContainerLogs_Tty_True_Follow_True_StreamLogs_TaskIsCancelled()
+ {
+ using var containerLogsCts = new CancellationTokenSource(TimeSpan.FromSeconds(60));
+ var logList = new List();
+
+ var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync(
+ new CreateContainerParameters()
+ {
+ Image = _imageId,
+ Name = Guid.NewGuid().ToString(),
+ Tty = true
+ },
+ _cts.Token
+ );
+
+ await _dockerClient.Containers.StartContainerAsync(
+ createContainerResponse.ID,
+ new ContainerStartParameters(),
+ _cts.Token
+ );
+
+ containerLogsCts.CancelAfter(TimeSpan.FromSeconds(5));
+
+ var containerLogsTask = _dockerClient.Containers.GetContainerLogsAsync(
+ createContainerResponse.ID,
+ new ContainerLogsParameters
+ {
+ ShowStderr = true,
+ ShowStdout = true,
+ Timestamps = true,
+ Follow = true
+ },
+ containerLogsCts.Token,
+ new Progress((m) => { _output.WriteLine(m); logList.Add(m); })
+ );
+
+ await Task.Delay(TimeSpan.FromSeconds(10));
+
+ await _dockerClient.Containers.StopContainerAsync(
+ createContainerResponse.ID,
+ new ContainerStopParameters
+ {
+ WaitBeforeKillSeconds = 0
+ },
+ _cts.Token
+ );
+
+ await _dockerClient.Containers.RemoveContainerAsync(
+ createContainerResponse.ID,
+ new ContainerRemoveParameters
+ {
+ Force = true
+ },
+ _cts.Token
+ );
+
+ await Assert.ThrowsAsync(() => containerLogsTask);
+
+ _output.WriteLine(JsonConvert.SerializeObject(new
+ {
+ AsyncState = containerLogsTask.AsyncState,
+ CreationOptions = containerLogsTask.CreationOptions,
+ Exception = containerLogsTask.Exception,
+ Id = containerLogsTask.Id,
+ IsCanceled = containerLogsTask.IsCanceled,
+ IsCompleted = containerLogsTask.IsCompleted,
+ IsCompletedSuccessfully = containerLogsTask.IsCompletedSuccessfully,
+ Status = containerLogsTask.Status
+ }
+ ));
+
+ _output.WriteLine($"Line count: {logList.Count}");
+
+ await Task.Delay(TimeSpan.FromSeconds(1));
+
+ Assert.NotEmpty(logList);
+ }
+
+ [Fact]
+ public async Task GetContainerLogs_Tty_True_ReadsLogs()
+ {
+ using var containerLogsCts = new CancellationTokenSource(TimeSpan.FromSeconds(60));
+ var logList = new List();
+
+ var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync(
+ new CreateContainerParameters()
+ {
+ Image = _imageId,
+ Name = Guid.NewGuid().ToString(),
+ Tty = true
+ },
+ _cts.Token
+ );
+
+ await _dockerClient.Containers.StartContainerAsync(
+ createContainerResponse.ID,
+ new ContainerStartParameters(),
+ _cts.Token
+ );
+
+ containerLogsCts.CancelAfter(TimeSpan.FromSeconds(5));
+
+ var containerLogsTask = _dockerClient.Containers.GetContainerLogsAsync(
+ createContainerResponse.ID,
+ new ContainerLogsParameters
+ {
+ ShowStderr = true,
+ ShowStdout = true,
+ Timestamps = true,
+ Follow = false
+ },
+ containerLogsCts.Token,
+ new Progress((m) => { _output.WriteLine(m); logList.Add(m); })
+ );
+
+ await Task.Delay(TimeSpan.FromSeconds(10));
+
+ await _dockerClient.Containers.StopContainerAsync(
+ createContainerResponse.ID,
+ new ContainerStopParameters
+ {
+ WaitBeforeKillSeconds = 0
+ },
+ _cts.Token
+ );
+
+ await _dockerClient.Containers.RemoveContainerAsync(
+ createContainerResponse.ID,
+ new ContainerRemoveParameters
+ {
+ Force = true
+ },
+ _cts.Token
+ );
+
+ await containerLogsTask;
+
+ _output.WriteLine(JsonConvert.SerializeObject(new
+ {
+ AsyncState = containerLogsTask.AsyncState,
+ CreationOptions = containerLogsTask.CreationOptions,
+ Exception = containerLogsTask.Exception,
+ Id = containerLogsTask.Id,
+ IsCanceled = containerLogsTask.IsCanceled,
+ IsCompleted = containerLogsTask.IsCompleted,
+ IsCompletedSuccessfully = containerLogsTask.IsCompletedSuccessfully,
+ Status = containerLogsTask.Status
+ }
+ ));
+
+ _output.WriteLine($"Line count: {logList.Count}");
+
+ Assert.NotEmpty(logList);
+ }
+
+ [Fact]
+ public async Task GetContainerStatsAsync_Tty_False_Stream_False_ReadsStats()
+ {
+ using var tcs = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token);
+ var containerStatsList = new List();
+
+ var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync(
+ new CreateContainerParameters
+ {
+ Image = _imageId,
+ Name = Guid.NewGuid().ToString(),
+ Tty = false
+ },
+ _cts.Token
+ );
+
+ _ = await _dockerClient.Containers.StartContainerAsync(
+ createContainerResponse.ID,
+ new ContainerStartParameters(),
+ _cts.Token
+ );
+
+ tcs.CancelAfter(TimeSpan.FromSeconds(10));
+
+ await _dockerClient.Containers.GetContainerStatsAsync(
+ createContainerResponse.ID,
+ new ContainerStatsParameters
+ {
+ Stream = false
+ },
+ new Progress((m) => { _output.WriteLine(m.ID); containerStatsList.Add(m); }),
+ tcs.Token
+ );
+
+ await Task.Delay(TimeSpan.FromSeconds(10));
+
+ Assert.NotEmpty(containerStatsList);
+ Assert.Single(containerStatsList);
+ _output.WriteLine($"ConntainerStats count: {containerStatsList.Count}");
+ }
+
+ [Fact]
+ public async Task GetContainerStatsAsync_Tty_False_StreamStats()
+ {
+ using var tcs = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token);
+ using (tcs.Token.Register(() => throw new TimeoutException("GetContainerStatsAsync_Tty_False_StreamStats")))
+ {
+ _output.WriteLine($"Running test {MethodBase.GetCurrentMethod().Module}->{MethodBase.GetCurrentMethod().Name}");
+
+ var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync(
+ new CreateContainerParameters()
+ {
+ Image = _imageId,
+ Name = Guid.NewGuid().ToString(),
+ Tty = false
+ },
+ _cts.Token
+ );
+
+ _ = await _dockerClient.Containers.StartContainerAsync(
+ createContainerResponse.ID,
+ new ContainerStartParameters(),
+ _cts.Token
+ );
+
+ List containerStatsList = new List();
+
+ using var linkedCts = new CancellationTokenSource();
+ linkedCts.CancelAfter(TimeSpan.FromSeconds(5));
+ try
+ {
+ await _dockerClient.Containers.GetContainerStatsAsync(
+ createContainerResponse.ID,
+ new ContainerStatsParameters
+ {
+ Stream = true
+ },
+ new Progress((m) => { containerStatsList.Add(m); _output.WriteLine(JsonConvert.SerializeObject(m)); }),
+ linkedCts.Token
+ );
+ }
+ catch (TaskCanceledException)
+ {
+ // this is expected to happen on task cancelaltion
+ }
+
+ _output.WriteLine($"Container stats count: {containerStatsList.Count}");
+ Assert.NotEmpty(containerStatsList);
+ }
+ }
+
+ [Fact]
+ public async Task GetContainerStatsAsync_Tty_True_Stream_False_ReadsStats()
+ {
+ using var tcs = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token);
+ var containerStatsList = new List();
+
+ var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync(
+ new CreateContainerParameters
+ {
+ Image = _imageId,
+ Name = Guid.NewGuid().ToString(),
+ Tty = true
+ },
+ _cts.Token
+ );
+
+ _ = await _dockerClient.Containers.StartContainerAsync(
+ createContainerResponse.ID,
+ new ContainerStartParameters(),
+ _cts.Token
+ );
+
+ tcs.CancelAfter(TimeSpan.FromSeconds(10));
+
+ await _dockerClient.Containers.GetContainerStatsAsync(
+ createContainerResponse.ID,
+ new ContainerStatsParameters
+ {
+ Stream = false
+ },
+ new Progress((m) => { _output.WriteLine(m.ID); containerStatsList.Add(m); }),
+ tcs.Token
+ );
+
+ await Task.Delay(TimeSpan.FromSeconds(10));
+
+ Assert.NotEmpty(containerStatsList);
+ Assert.Single(containerStatsList);
+ _output.WriteLine($"ConntainerStats count: {containerStatsList.Count}");
+ }
+
+ [Fact]
+ public async Task GetContainerStatsAsync_Tty_True_StreamStats()
+ {
+ using var tcs = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token);
+
+ using (tcs.Token.Register(() => throw new TimeoutException("GetContainerStatsAsync_Tty_True_StreamStats")))
+ {
+ _output.WriteLine($"Running test GetContainerStatsAsync_Tty_True_StreamStats");
+
+ var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync(
+ new CreateContainerParameters()
+ {
+ Image = _imageId,
+ Name = Guid.NewGuid().ToString(),
+ Tty = true
+ },
+ _cts.Token
+ );
+
+ _ = await _dockerClient.Containers.StartContainerAsync(
+ createContainerResponse.ID,
+ new ContainerStartParameters(),
+ _cts.Token
+ );
+
+ List containerStatsList = new List();
+
+ using var linkedTcs = CancellationTokenSource.CreateLinkedTokenSource(tcs.Token);
+ linkedTcs.CancelAfter(TimeSpan.FromSeconds(5));
+
+ try
+ {
+ await _dockerClient.Containers.GetContainerStatsAsync(
+ createContainerResponse.ID,
+ new ContainerStatsParameters
+ {
+ Stream = true
+ },
+ new Progress((m) => { containerStatsList.Add(m); _output.WriteLine(JsonConvert.SerializeObject(m)); }),
+ linkedTcs.Token
+ );
+ }
+ catch (TaskCanceledException)
+ {
+ // this is expected to happen on task cancelaltion
+ }
+
+ await Task.Delay(TimeSpan.FromSeconds(1));
+ _output.WriteLine($"Container stats count: {containerStatsList.Count}");
+ Assert.NotEmpty(containerStatsList);
+ }
+ }
+
+ [Fact]
+ public async Task KillContainerAsync_ContainerRunning_Succeeds()
+ {
+ var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync(
+ new CreateContainerParameters
+ {
+ Image = _imageId
+ },
+ _cts.Token);
+
+ await _dockerClient.Containers.StartContainerAsync(
+ createContainerResponse.ID,
+ new ContainerStartParameters(),
+ _cts.Token
+ );
+
+ var inspectRunningContainerResponse = await _dockerClient.Containers.InspectContainerAsync(
+ createContainerResponse.ID,
+ _cts.Token);
+
+ await _dockerClient.Containers.KillContainerAsync(
+ createContainerResponse.ID,
+ new ContainerKillParameters(),
+ _cts.Token);
+
+ var inspectKilledContainerResponse = await _dockerClient.Containers.InspectContainerAsync(
+ createContainerResponse.ID,
+ _cts.Token);
+
+ Assert.True(inspectRunningContainerResponse.State.Running);
+ Assert.False(inspectKilledContainerResponse.State.Running);
+ Assert.Equal("exited", inspectKilledContainerResponse.State.Status);
+
+ _output.WriteLine("Killed");
+ _output.WriteLine(JsonConvert.SerializeObject(inspectKilledContainerResponse));
+ }
+
+ [Fact]
+ public async Task ListContainersAsync_ContainerExists_Succeeds()
+ {
+ await _dockerClient.Containers.CreateContainerAsync(new CreateContainerParameters()
+ {
+ Image = _imageId,
+ Name = Guid.NewGuid().ToString()
+ },
+ _cts.Token);
+
+ IList containerList = await _dockerClient.Containers.ListContainersAsync(
+ new ContainersListParameters
+ {
+ Filters = new Dictionary>
+ {
+ ["ancestor"] = new Dictionary
+ {
+ [_imageId] = true
+ }
+ },
+ All = true
+ },
+ _cts.Token
+ );
+
+ Assert.NotNull(containerList);
+ Assert.NotEmpty(containerList);
+ }
+
+ [Fact]
+ public async Task ListProcessesAsync_RunningContainer_Succeeds()
+ {
+ var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync(
+ new CreateContainerParameters()
+ {
+ Image = _imageId,
+ Name = Guid.NewGuid().ToString()
+ },
+ _cts.Token
+ );
+
+ await _dockerClient.Containers.StartContainerAsync(
+ createContainerResponse.ID,
+ new ContainerStartParameters(),
+ _cts.Token
+ );
+
+ var containerProcessesResponse = await _dockerClient.Containers.ListProcessesAsync(
+ createContainerResponse.ID,
+ new ContainerListProcessesParameters(),
+ _cts.Token
+ );
+
+ _output.WriteLine($"Title '{containerProcessesResponse.Titles[0]}' - '{containerProcessesResponse.Titles[1]}' - '{containerProcessesResponse.Titles[2]}' - '{containerProcessesResponse.Titles[3]}'");
+
+ foreach (var processes in containerProcessesResponse.Processes)
+ {
+ _output.WriteLine($"Process '{processes[0]}' - ''{processes[1]}' - '{processes[2]}' - '{processes[3]}'");
+ }
+
+ Assert.NotNull(containerProcessesResponse);
+ Assert.NotEmpty(containerProcessesResponse.Processes);
+ }
+
+ [Fact]
+ public async Task RemoveContainerAsync_ContainerExists_Succeedes()
+ {
+ var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync(
+ new CreateContainerParameters()
+ {
+ Image = _imageId,
+ Name = Guid.NewGuid().ToString()
+ },
+ _cts.Token
+ );
+
+ ContainerInspectResponse inspectCreatedContainer = await _dockerClient.Containers.InspectContainerAsync(
+ createContainerResponse.ID,
+ _cts.Token
+ );
+
+ await _dockerClient.Containers.RemoveContainerAsync(
+ createContainerResponse.ID,
+ new ContainerRemoveParameters
+ {
+ Force = true
+ },
+ _cts.Token
+ );
+
+ Task inspectRemovedContainerTask = _dockerClient.Containers.InspectContainerAsync(
+ createContainerResponse.ID,
+ _cts.Token
+ );
+
+ Assert.NotNull(inspectCreatedContainer.State);
+ await Assert.ThrowsAsync(() => inspectRemovedContainerTask);
+ }
+
+ [Fact]
+ public async Task StartContainerAsync_ContainerExists_Succeeds()
+ {
+ var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync(
+ new CreateContainerParameters()
+ {
+ Image = _imageId,
+ Name = Guid.NewGuid().ToString()
+ },
+ _cts.Token
+ );
+
+ var startContainerResult = await _dockerClient.Containers.StartContainerAsync(
+ createContainerResponse.ID,
+ new ContainerStartParameters(),
+ _cts.Token
+ );
+
+ Assert.True(startContainerResult);
+ }
+
+ [Fact]
+ public async Task StartContainerAsync_ContainerNotExists_ThrowsException()
+ {
+ Task startContainerTask = _dockerClient.Containers.StartContainerAsync(
+ Guid.NewGuid().ToString(),
+ new ContainerStartParameters(),
+ _cts.Token
+ );
+
+ await Assert.ThrowsAsync(() => startContainerTask);
+ }
+
+ [Fact]
+ public async Task WaitContainerAsync_TokenIsCancelled_OperationCancelledException()
+ {
+ var stopWatch = new Stopwatch();
+
+ using var waitContainerCts = new CancellationTokenSource(delay: TimeSpan.FromMinutes(5));
+
+ var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync(
+ new CreateContainerParameters
+ {
+ Image = _imageId,
+ Name = Guid.NewGuid().ToString(),
+ },
+ waitContainerCts.Token
+ );
+
+ _output.WriteLine($"CreateContainerResponse: '{JsonConvert.SerializeObject(createContainerResponse)}'");
+
+ var startContainerResult = await _dockerClient.Containers.StartContainerAsync(createContainerResponse.ID, new ContainerStartParameters(), waitContainerCts.Token);
+
+ _output.WriteLine("Starting timeout to cancel WaitContainer operation.");
+
+ TimeSpan delay = TimeSpan.FromSeconds(5);
+
+ waitContainerCts.CancelAfter(delay);
+ stopWatch.Start();
+
+ // Will wait forever here if cancelation fails.
+ var waitContainerTask = _dockerClient.Containers.WaitContainerAsync(createContainerResponse.ID, waitContainerCts.Token);
+
+ var exception = await Assert.ThrowsAsync(() => waitContainerTask);
+
+ stopWatch.Stop();
+
+ _output.WriteLine($"WaitContainerTask was cancelled after {stopWatch.ElapsedMilliseconds} ms");
+ _output.WriteLine($"WaitContainerAsync: {stopWatch.Elapsed} elapsed");
+
+ // Task should be cancelled when CancelAfter timespan expires
+ TimeSpan tolerance = TimeSpan.FromMilliseconds(500);
+
+ Assert.InRange(stopWatch.Elapsed, delay.Subtract(tolerance), delay.Add(tolerance));
+ Assert.True(waitContainerTask.IsCanceled);
+ }
+ }
+}
diff --git a/test/Docker.DotNet.Tests/IImageOperationsTests.cs b/test/Docker.DotNet.Tests/IImageOperationsTests.cs
new file mode 100644
index 000000000..908d7fa63
--- /dev/null
+++ b/test/Docker.DotNet.Tests/IImageOperationsTests.cs
@@ -0,0 +1,109 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Docker.DotNet.Models;
+using Newtonsoft.Json;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Docker.DotNet.Tests
+{
+ [Collection("Test collection")]
+ public class IImageOperationsTests
+ {
+
+ private readonly CancellationTokenSource _cts;
+
+ private readonly TestOutput _output;
+ private readonly string _repositoryName;
+ private readonly string _tag = Guid.NewGuid().ToString();
+ private readonly DockerClientConfiguration _dockerConfiguration;
+ private readonly DockerClient _dockerClient;
+
+ public IImageOperationsTests(TestFixture testFixture, ITestOutputHelper _outputHelper)
+ {
+ _output = new TestOutput(_outputHelper);
+
+ _dockerConfiguration = new DockerClientConfiguration();
+ _dockerClient = _dockerConfiguration.CreateClient();
+
+ // Do not wait forever in case it gets stuck
+ _cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
+ _cts.Token.Register(() => throw new TimeoutException("ImageOperationTests timeout"));
+
+ _repositoryName = testFixture.repositoryName;
+ _tag = testFixture.tag;
+ }
+
+ [Fact]
+ public async Task CreateImageAsync_TaskCancelled_ThowsTaskCanceledException()
+ {
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token);
+
+ var newTag = Guid.NewGuid().ToString();
+ var newRepositoryName = Guid.NewGuid().ToString();
+
+ await _dockerClient.Images.TagImageAsync(
+ $"{_repositoryName}:{_tag}",
+ new ImageTagParameters
+ {
+ RepositoryName = newRepositoryName,
+ Tag = newTag,
+ Force = true
+ },
+ cts.Token
+ );
+
+ var createImageTask = _dockerClient.Images.CreateImageAsync(
+ new ImagesCreateParameters
+ {
+ FromImage = $"{newRepositoryName}:{newTag}"
+ },
+ null,
+ new Progress((message) => _output.WriteLine(JsonConvert.SerializeObject(message))),
+ cts.Token);
+
+ TimeSpan delay = TimeSpan.FromMilliseconds(5);
+ cts.CancelAfter(delay);
+
+ await Assert.ThrowsAsync(() => createImageTask);
+
+ Assert.True(createImageTask.IsCanceled);
+ }
+
+ [Fact]
+ public async Task DeleteImageAsync_RemovesImage()
+ {
+ var newImageTag = Guid.NewGuid().ToString();
+
+ await _dockerClient.Images.TagImageAsync(
+ $"{_repositoryName}:{_tag}",
+ new ImageTagParameters
+ {
+ RepositoryName = _repositoryName,
+ Tag = newImageTag
+ },
+ _cts.Token
+ );
+
+ var inspectExistingImageResponse = await _dockerClient.Images.InspectImageAsync(
+ $"{_repositoryName}:{newImageTag}",
+ _cts.Token
+ );
+
+ await _dockerClient.Images.DeleteImageAsync(
+ $"{_repositoryName}:{newImageTag}",
+ new ImageDeleteParameters(),
+ _cts.Token
+ );
+
+ Task inspectDeletedImageTask = _dockerClient.Images.InspectImageAsync(
+ $"{_repositoryName}:{newImageTag}",
+ _cts.Token
+ );
+
+ Assert.NotNull(inspectExistingImageResponse);
+ await Assert.ThrowsAsync(() => inspectDeletedImageTask);
+ }
+ }
+}
diff --git a/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs b/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs
index 7340a83de..3b930116d 100644
--- a/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs
+++ b/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -7,21 +7,16 @@
namespace Docker.DotNet.Tests
{
- public class ISwarmOperationsTests : IDisposable
+ [Collection("Test collection")]
+ public class ISwarmOperationsTests
{
private readonly DockerClient _client;
+ private readonly string _imageId;
- public ISwarmOperationsTests()
+ public ISwarmOperationsTests(TestFixture testFixture)
{
- using var configuration = new DockerClientConfiguration();
- _client = configuration.CreateClient();
-
- // Init swarm if not part of one
- try
- {
- var result = _client.Swarm.InitSwarmAsync(new SwarmInitParameters { AdvertiseAddr = "10.10.10.10", ListenAddr = "127.0.0.1" }, default).Result;
- }
- catch { }
+ _client = testFixture.dockerClient;
+ _imageId = testFixture.imageId;
}
[Fact]
@@ -33,7 +28,7 @@ public async Task GetFilteredServicesByName_Succeeds()
Service = new ServiceSpec
{
Name = firstServiceName,
- TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = "hello-world" } }
+ TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } }
}
}).Result.ID;
@@ -42,7 +37,7 @@ public async Task GetFilteredServicesByName_Succeeds()
Service = new ServiceSpec
{
Name = $"service2-{Guid.NewGuid().ToString().Substring(1, 10)}",
- TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = "hello-world" } }
+ TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } }
}
}).Result.ID;
@@ -51,7 +46,7 @@ public async Task GetFilteredServicesByName_Succeeds()
Service = new ServiceSpec
{
Name = $"service3-{Guid.NewGuid().ToString().Substring(1, 10)}",
- TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = "hello-world" } }
+ TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } }
}
}).Result.ID;
@@ -83,7 +78,7 @@ public async Task GetFilteredServicesById_Succeeds()
Service = new ServiceSpec
{
Name = $"service1-{Guid.NewGuid().ToString().Substring(1, 10)}",
- TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = "hello-world" } }
+ TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } }
}
}).Result.ID;
@@ -92,7 +87,7 @@ public async Task GetFilteredServicesById_Succeeds()
Service = new ServiceSpec
{
Name = $"service2-{Guid.NewGuid().ToString().Substring(1, 10)}",
- TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = "hello-world" } }
+ TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } }
}
}).Result.ID;
@@ -101,7 +96,7 @@ public async Task GetFilteredServicesById_Succeeds()
Service = new ServiceSpec
{
Name = $"service3-{Guid.NewGuid().ToString().Substring(1, 10)}",
- TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = "hello-world" } }
+ TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } }
}
}).Result.ID;
@@ -123,7 +118,7 @@ public async Task GetServices_Succeeds()
Service = new ServiceSpec
{
Name = $"service1-{Guid.NewGuid().ToString().Substring(1, 10)}",
- TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = "hello-world" } }
+ TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } }
}
}).Result.ID;
@@ -132,7 +127,7 @@ public async Task GetServices_Succeeds()
Service = new ServiceSpec
{
Name = $"service2-{Guid.NewGuid().ToString().Substring(1, 10)}",
- TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = "hello-world" } }
+ TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } }
}
}).Result.ID;
@@ -141,7 +136,7 @@ public async Task GetServices_Succeeds()
Service = new ServiceSpec
{
Name = $"service3-{Guid.NewGuid().ToString().Substring(1, 10)}",
- TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = "hello-world" } }
+ TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } }
}
}).Result.ID;
@@ -153,15 +148,5 @@ public async Task GetServices_Succeeds()
await _client.Swarm.RemoveServiceAsync(secondServiceId, default);
await _client.Swarm.RemoveServiceAsync(thirdServiceid, default);
}
-
- public void Dispose()
- {
- //if (!wasSwarmInitialized)
- //{
- // _client.Swarm.LeaveSwarmAsync(new SwarmLeaveParameters { Force = true });
- //}
-
- GC.SuppressFinalize(this);
- }
}
}
diff --git a/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs b/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs
index cd17102f2..5eb2d51cb 100644
--- a/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs
+++ b/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs
@@ -2,20 +2,36 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Docker.DotNet.Models;
+using Newtonsoft.Json;
using Xunit;
+using Xunit.Abstractions;
namespace Docker.DotNet.Tests
{
+ [Collection("Test collection")]
public class ISystemOperationsTests
{
private readonly DockerClient _client;
+ private readonly TestOutput _output;
+ private readonly string _repositoryName;
+ private readonly string _tag;
+ private readonly CancellationTokenSource _cts;
- public ISystemOperationsTests()
+ public ISystemOperationsTests(TestFixture testFixture, ITestOutputHelper output)
{
- _client = new DockerClientConfiguration().CreateClient();
+ _cts = CancellationTokenSource.CreateLinkedTokenSource(testFixture.cts.Token);
+ _cts.Token.Register(() => throw new TimeoutException("ISystemOperationsTest timeout"));
+ _cts.CancelAfter(TimeSpan.FromMinutes(5));
+
+ _repositoryName = testFixture.repositoryName;
+ _tag = testFixture.tag;
+
+ _client = testFixture.dockerClient;
+ _output = new TestOutput(output);
}
[Fact]
@@ -42,30 +58,14 @@ public async Task GetVersionAsync_Succeeds()
[Fact]
public async Task MonitorEventsAsync_EmptyContainersList_CanBeCancelled()
{
- var progress = new ProgressMessage()
- {
- _onMessageCalled = (m) => { }
- };
-
- var cts = new CancellationTokenSource();
- cts.CancelAfter(1000);
+ var progress = new Progress();
- var task = _client.System.MonitorEventsAsync(new ContainerEventsParameters(), progress, cts.Token);
+ using var cts = new CancellationTokenSource();
+ cts.Cancel();
+ await Task.Delay(1);
- var taskIsCancelled = false;
- try
- {
- await task;
- }
- catch
- {
- // Exception is not always thrown when cancelling token
- taskIsCancelled = true;
- }
+ await Assert.ThrowsAsync(() => _client.System.MonitorEventsAsync(new ContainerEventsParameters(), progress, cts.Token));
- // On local develop machine task is completed.
- // On CI/CD Pipeline exception is thrown, not always
- Assert.True(task.IsCompleted || taskIsCancelled);
}
[Fact]
@@ -83,70 +83,140 @@ public async Task MonitorEventsAsync_NullProgress_Throws()
[Fact]
public async Task MonitorEventsAsync_Succeeds()
{
- const string repository = "hello-world";
var newTag = $"MonitorTests-{Guid.NewGuid().ToString().Substring(1, 10)}";
- var progressJSONMessage = new ProgressJSONMessage
+ var progressJSONMessage = new Progress((m) =>
{
- _onJSONMessageCalled = (m) =>
- {
- // Status could be 'Pulling from...'
- Console.WriteLine($"{System.Reflection.MethodInfo.GetCurrentMethod().Module}->{System.Reflection.MethodInfo.GetCurrentMethod().Name}: _onJSONMessageCalled - {m.ID} - {m.Status} {m.From} - {m.Stream}");
- Assert.NotNull(m);
- }
- };
+ // Status could be 'Pulling from...'
+ Assert.NotNull(m);
+ _output.WriteLine($"MonitorEventsAsync_Succeeds: JSONMessage - {m.ID} - {m.Status} {m.From} - {m.Stream}");
+ });
var wasProgressCalled = false;
- var progressMessage = new ProgressMessage
+
+ var progressMessage = new Progress((m) =>
{
- _onMessageCalled = (m) =>
- {
- Console.WriteLine($"{System.Reflection.MethodInfo.GetCurrentMethod().Module}->{System.Reflection.MethodInfo.GetCurrentMethod().Name}: _onMessageCalled - {m.Action} - {m.Status} {m.From} - {m.Type}");
- wasProgressCalled = true;
- Assert.NotNull(m);
- }
- };
+ _output.WriteLine($"MonitorEventsAsync_Succeeds: Message - {m.Action} - {m.Status} {m.From} - {m.Type}");
+ wasProgressCalled = true;
+ Assert.NotNull(m);
+ });
- using var cts = new CancellationTokenSource();
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token);
- await _client.Images.CreateImageAsync(new ImagesCreateParameters { FromImage = "hello-world" }, null, progressJSONMessage);
+ var task = _client.System.MonitorEventsAsync(
+ new ContainerEventsParameters(),
+ progressMessage,
+ cts.Token);
- var task = Task.Run(() => _client.System.MonitorEventsAsync(new ContainerEventsParameters(), progressMessage, cts.Token));
+ await _client.Images.CreateImageAsync(new ImagesCreateParameters { FromImage = $"{_repositoryName}:{_tag}" }, null, progressJSONMessage, _cts.Token);
- await _client.Images.TagImageAsync(repository, new ImageTagParameters { RepositoryName = repository, Tag = newTag });
+ await _client.Images.TagImageAsync($"{_repositoryName}:{_tag}", new ImageTagParameters { RepositoryName = _repositoryName, Tag = newTag }, _cts.Token);
+
+ await _client.Images.DeleteImageAsync(
+ name: $"{_repositoryName}:{newTag}",
+ new ImageDeleteParameters
+ {
+ Force = true
+ },
+ _cts.Token);
+
+ // Give it some time for output operation to complete before cancelling task
+ await Task.Delay(TimeSpan.FromSeconds(1));
cts.Cancel();
- bool taskIsCancelled = false;
- try
- {
- await task;
- }
- catch (OperationCanceledException)
- {
- taskIsCancelled = true;
- }
+ await Assert.ThrowsAsync(() => task).ConfigureAwait(false);
- // On local develop machine task is completed.
- // On CI/CD Pipeline exception is thrown, not always
- Assert.True(task.IsCompleted || taskIsCancelled);
Assert.True(wasProgressCalled);
+ }
+
+ [Fact]
+ public async Task MonitorEventsAsync_IsCancelled_NoStreamCorruption()
+ {
+ var rand = new Random();
+ var sw = new Stopwatch();
+
+ for (int i = 0; i < 20; ++i)
+ {
+ try
+ {
+ // (1) Create monitor task
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token);
+
+ string newImageTag = Guid.NewGuid().ToString();
+
+ var monitorTask = _client.System.MonitorEventsAsync(
+ new ContainerEventsParameters(),
+ new Progress((value) => _output.WriteLine($"DockerSystemEvent: {JsonConvert.SerializeObject(value)}")),
+ cts.Token);
+
+ // (2) Wait for some time to make sure we get into blocking IO call
+ await Task.Delay(100);
+
+ // (3) Invoke another request that will attempt to grab the same buffer
+ var listImagesTask1 = _client.Images.TagImageAsync(
+ $"{_repositoryName}:{_tag}",
+ new ImageTagParameters
+ {
+ RepositoryName = _repositoryName,
+ Tag = newImageTag,
+ Force = true
+ },
+ default);
+
+ // (4) Wait for a short bit again and cancel the monitor task - if we get lucky, we the list images call will grab the same buffer while
+ sw.Restart();
+ var iterations = rand.Next(15000000);
+
+ for (int j = 0; j < iterations; j++)
+ {
+ // noop
+ }
+ _output.WriteLine($"Waited for {sw.Elapsed.TotalMilliseconds} ms");
+
+ cts.Cancel();
+
+ listImagesTask1.GetAwaiter().GetResult();
+
+ _client.Images.TagImageAsync(
+ $"{_repositoryName}:{_tag}",
+ new ImageTagParameters
+ {
+ RepositoryName = _repositoryName,
+ Tag = newImageTag,
+ Force = true
+ }
+ ).GetAwaiter().GetResult();
- await _client.Images.DeleteImageAsync($"{repository}:{newTag}", new ImageDeleteParameters());
+ monitorTask.GetAwaiter().GetResult();
+ }
+ catch (TaskCanceledException)
+ {
+ // Exceptions other than this causes test to fail
+ }
+ }
}
[Fact]
public async Task MonitorEventsFiltered_Succeeds()
{
- const string repository = "hello-world";
- var newTag = $"MonitorTests-{Guid.NewGuid().ToString().Substring(1, 10)}";
+ string newTag = $"MonitorTests-{Guid.NewGuid().ToString().Substring(1, 10)}";
+ string newImageRespositoryName = Guid.NewGuid().ToString();
- var progressJSONMessage = new ProgressJSONMessage
- {
- _onJSONMessageCalled = (m) => { }
- };
+ await _client.Images.TagImageAsync(
+ $"{_repositoryName}:{_tag}",
+ new ImageTagParameters
+ {
+ RepositoryName = newImageRespositoryName,
+ Tag = newTag
+ },
+ _cts.Token
+ );
- await _client.Images.CreateImageAsync(new ImagesCreateParameters { FromImage = repository }, null, progressJSONMessage);
+ ImageInspectResponse image = await _client.Images.InspectImageAsync(
+ $"{newImageRespositoryName}:{newTag}",
+ _cts.Token
+ );
var progressCalledCounter = 0;
@@ -172,46 +242,43 @@ public async Task MonitorEventsFiltered_Succeeds()
"image", true
}
}
+ },
+ {
+ "image", new Dictionary()
+ {
+ {
+ image.ID, true
+ }
+ }
}
}
};
- var progress = new ProgressMessage()
+ var progress = new Progress((m) =>
{
- _onMessageCalled = (m) =>
- {
- Console.WriteLine($"{System.Reflection.MethodInfo.GetCurrentMethod().Module}->{System.Reflection.MethodInfo.GetCurrentMethod().Name}: _onMessageCalled received: {m.Action} - {m.Status} {m.From} - {m.Type}");
- Assert.True(m.Status == "tag" || m.Status == "untag");
- progressCalledCounter++;
- }
- };
+ progressCalledCounter++;
+ Assert.True(m.Status == "tag" || m.Status == "untag");
+ _output.WriteLine($"MonitorEventsFiltered_Succeeds: Message received: {m.Action} - {m.Status} {m.From} - {m.Type}");
+ });
- using var cts = new CancellationTokenSource();
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token);
var task = Task.Run(() => _client.System.MonitorEventsAsync(eventsParams, progress, cts.Token));
- await _client.Images.CreateImageAsync(new ImagesCreateParameters { FromImage = repository }, null, progressJSONMessage);
+ await _client.Images.CreateImageAsync(new ImagesCreateParameters { FromImage = $"{_repositoryName}:{_tag}" }, null, new Progress());
- await _client.Images.TagImageAsync(repository, new ImageTagParameters { RepositoryName = repository, Tag = newTag });
- await _client.Images.DeleteImageAsync($"{repository}:{newTag}", new ImageDeleteParameters());
+ await _client.Images.TagImageAsync($"{_repositoryName}:{_tag}", new ImageTagParameters { RepositoryName = _repositoryName, Tag = newTag });
+ await _client.Images.DeleteImageAsync($"{_repositoryName}:{newTag}", new ImageDeleteParameters());
- var newContainerId = _client.Containers.CreateContainerAsync(new CreateContainerParameters { Image = repository }).Result.ID;
- await _client.Containers.RemoveContainerAsync(newContainerId, new ContainerRemoveParameters(), cts.Token);
+ var createContainerResponse = await _client.Containers.CreateContainerAsync(new CreateContainerParameters { Image = $"{_repositoryName}:{_tag}" });
+ await _client.Containers.RemoveContainerAsync(createContainerResponse.ID, new ContainerRemoveParameters(), cts.Token);
+ await Task.Delay(TimeSpan.FromSeconds(1));
cts.Cancel();
- bool taskIsCancelled = false;
- try
- {
- await task;
- }
- catch (OperationCanceledException)
- {
- taskIsCancelled = true;
- }
- // On local develop machine task is completed.
- // On CI/CD Pipeline exception is thrown, not always
- Assert.True(task.IsCompleted || taskIsCancelled);
+ await Assert.ThrowsAsync(() => task);
+
Assert.Equal(2, progressCalledCounter);
+ Assert.True(task.IsCanceled);
}
[Fact]
@@ -219,25 +286,5 @@ public async Task PingAsync_Succeeds()
{
await _client.System.PingAsync();
}
-
- private class ProgressMessage : IProgress
- {
- internal Action _onMessageCalled;
-
- void IProgress.Report(Message value)
- {
- _onMessageCalled(value);
- }
- }
-
- private class ProgressJSONMessage : IProgress
- {
- internal Action _onJSONMessageCalled;
-
- void IProgress.Report(JSONMessage value)
- {
- _onJSONMessageCalled(value);
- }
- }
}
}
diff --git a/test/Docker.DotNet.Tests/SupportedOSPlatformsFactAttribute.cs b/test/Docker.DotNet.Tests/SupportedOSPlatformsFactAttribute.cs
deleted file mode 100644
index dc0cd61f0..000000000
--- a/test/Docker.DotNet.Tests/SupportedOSPlatformsFactAttribute.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using System;
-using System.Linq;
-using System.Runtime.InteropServices;
-using Xunit;
-
-namespace Docker.DotNet.Tests
-{
- public enum Platform
- {
- Linux,
- OSX,
- Windows
- }
-
- public sealed class SupportedOSPlatformsFactAttribute : FactAttribute
- {
- private static Platform CurrentPlatform()
- {
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
- return Platform.Linux;
- if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
- return Platform.OSX;
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- return Platform.Windows;
- throw new PlatformNotSupportedException();
- }
-
- public SupportedOSPlatformsFactAttribute(params Platform[] supportedPlatforms)
- {
- var currentPlatform = CurrentPlatform();
- var isSupported = supportedPlatforms.Contains(currentPlatform);
- Skip = isSupported ? null : $"Not applicable to {currentPlatform}";
- }
- }
-}
\ No newline at end of file
diff --git a/test/Docker.DotNet.Tests/TestFixture.cs b/test/Docker.DotNet.Tests/TestFixture.cs
new file mode 100644
index 000000000..e1e77b4ad
--- /dev/null
+++ b/test/Docker.DotNet.Tests/TestFixture.cs
@@ -0,0 +1,161 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Threading;
+using Docker.DotNet.Models;
+using Newtonsoft.Json;
+using Xunit;
+
+namespace Docker.DotNet.Tests
+{
+ public class TestFixture : IDisposable
+ {
+ // Tests require an image whose containers continue running when created new, and works on both Windows an Linux containers.
+ private const string _imageName = "nats";
+
+ private readonly bool _wasSwarmInitialized = false;
+
+ public TestFixture()
+ {
+ // Do not wait forever in case it gets stuck
+ cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
+ cts.Token.Register(() => throw new TimeoutException("Docker.DotNet test timeout exception"));
+
+ dockerClientConfiguration = new DockerClientConfiguration();
+ dockerClient = dockerClientConfiguration.CreateClient();
+
+ // Create image
+ dockerClient.Images.CreateImageAsync(
+ new ImagesCreateParameters
+ {
+ FromImage = _imageName,
+ Tag = "latest"
+ },
+ null,
+ new Progress((m) => { Console.WriteLine(JsonConvert.SerializeObject(m)); Debug.WriteLine(JsonConvert.SerializeObject(m)); }),
+ cts.Token).GetAwaiter().GetResult();
+
+ // Create local image tag to reuse
+ var existingImagesResponse = dockerClient.Images.ListImagesAsync(
+ new ImagesListParameters
+ {
+ Filters = new Dictionary>
+ {
+ ["reference"] = new Dictionary
+ {
+ [_imageName] = true
+ }
+ }
+ },
+ cts.Token
+ ).GetAwaiter().GetResult();
+
+ imageId = existingImagesResponse[0].ID;
+
+ dockerClient.Images.TagImageAsync(
+ imageId,
+ new ImageTagParameters
+ {
+ RepositoryName = repositoryName,
+ Tag = tag
+ },
+ cts.Token
+ ).GetAwaiter().GetResult();
+
+ // Init swarm if not part of one
+ try
+ {
+ var result = dockerClient.Swarm.InitSwarmAsync(new SwarmInitParameters { AdvertiseAddr = "10.10.10.10", ListenAddr = "127.0.0.1" }, default).GetAwaiter().GetResult();
+ }
+ catch
+ {
+ Console.WriteLine("Couldn't init a new swarm, node should take part of a existing one");
+ _wasSwarmInitialized = true;
+ }
+
+
+ }
+
+ public CancellationTokenSource cts { get; }
+ public DockerClient dockerClient { get; }
+ public DockerClientConfiguration dockerClientConfiguration { get; }
+ public string repositoryName { get; } = Guid.NewGuid().ToString();
+ public string tag { get; } = Guid.NewGuid().ToString();
+ public string imageId { get; }
+
+ public void Dispose()
+ {
+ if (_wasSwarmInitialized)
+ {
+ dockerClient.Swarm.LeaveSwarmAsync(new SwarmLeaveParameters { Force = true }, cts.Token);
+ }
+
+ var containerList = dockerClient.Containers.ListContainersAsync(
+ new ContainersListParameters
+ {
+ Filters = new Dictionary>
+ {
+ ["ancestor"] = new Dictionary
+ {
+ [$"{repositoryName}:{tag}"] = true
+ }
+ },
+ All = true,
+ },
+ cts.Token
+ ).GetAwaiter().GetResult();
+
+ foreach (ContainerListResponse container in containerList)
+ {
+ dockerClient.Containers.RemoveContainerAsync(
+ container.ID,
+ new ContainerRemoveParameters
+ {
+ Force = true
+ },
+ cts.Token
+ ).GetAwaiter().GetResult();
+ }
+
+ var imageList = dockerClient.Images.ListImagesAsync(
+ new ImagesListParameters
+ {
+ Filters = new Dictionary>
+ {
+ ["reference"] = new Dictionary
+ {
+ [imageId] = true
+ },
+ ["since"] = new Dictionary
+ {
+ [imageId] = true
+ }
+ },
+ All = true
+ },
+ cts.Token
+ ).GetAwaiter().GetResult();
+
+ foreach (ImagesListResponse image in imageList)
+ {
+ dockerClient.Images.DeleteImageAsync(
+ image.ID,
+ new ImageDeleteParameters { Force = true },
+ cts.Token
+ ).GetAwaiter().GetResult();
+ }
+
+ dockerClient.Dispose();
+ dockerClientConfiguration.Dispose();
+ cts.Dispose();
+ }
+ }
+
+ [CollectionDefinition("Test collection")]
+ public class TestsCollection : ICollectionFixture
+ {
+ // This class has no code, and is never created. Its purpose is simply
+ // to be the place to apply [CollectionDefinition] and all the
+ // ICollectionFixture<> interfaces.
+ }
+}
diff --git a/test/Docker.DotNet.Tests/TestOutput.cs b/test/Docker.DotNet.Tests/TestOutput.cs
new file mode 100644
index 000000000..1417d7a46
--- /dev/null
+++ b/test/Docker.DotNet.Tests/TestOutput.cs
@@ -0,0 +1,22 @@
+using System;
+using Xunit.Abstractions;
+
+namespace Docker.DotNet.Tests
+{
+ public class TestOutput
+ {
+ private readonly ITestOutputHelper _outputHelper;
+
+ public TestOutput(ITestOutputHelper outputHelper)
+ {
+ _outputHelper = outputHelper;
+ }
+
+ public void WriteLine(string line)
+ {
+ Console.WriteLine(line);
+ _outputHelper.WriteLine(line);
+ System.Diagnostics.Debug.WriteLine(line);
+ }
+ }
+}