From f0336bf812a5938300c328686a61e177521d7a6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Vives?= Date: Wed, 3 Mar 2021 19:31:12 +0100 Subject: [PATCH] Squash commit --- src/Docker.DotNet/Docker.DotNet.csproj | 1 + src/Docker.DotNet/DockerClient.cs | 20 +- src/Docker.DotNet/Endpoints/StreamUtil.cs | 152 ++-- src/Docker.DotNet/JsonSerializer.cs | 25 +- .../ChunkedReadStream.cs | 2 +- .../IContainerOperationsTests.cs | 812 ++++++++++++++++++ .../IImageOperationsTests.cs | 109 +++ .../ISwarmOperationsTests.cs | 47 +- .../ISystemOperations.Tests.cs | 267 +++--- .../SupportedOSPlatformsFactAttribute.cs | 35 - test/Docker.DotNet.Tests/TestFixture.cs | 161 ++++ test/Docker.DotNet.Tests/TestOutput.cs | 22 + 12 files changed, 1367 insertions(+), 286 deletions(-) create mode 100644 test/Docker.DotNet.Tests/IContainerOperationsTests.cs create mode 100644 test/Docker.DotNet.Tests/IImageOperationsTests.cs delete mode 100644 test/Docker.DotNet.Tests/SupportedOSPlatformsFactAttribute.cs create mode 100644 test/Docker.DotNet.Tests/TestFixture.cs create mode 100644 test/Docker.DotNet.Tests/TestOutput.cs 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); + } + } +}