Fix flakey tests by making them async (#144)
* Fix flakey tests by converting them to async and synchronizing code blocks * Use the same timeout for all tests * Use signals to gracefully shut down the mock server
This commit is contained in:
committed by
Brendan Burns
parent
5247597ef5
commit
194211b370
@@ -20,12 +20,14 @@ using Xunit.Abstractions;
|
||||
|
||||
namespace k8s.Tests
|
||||
{
|
||||
public class AuthTests
|
||||
: TestBase
|
||||
public class AuthTests
|
||||
{
|
||||
public AuthTests(ITestOutputHelper testOutput) : base(testOutput)
|
||||
private readonly ITestOutputHelper testOutput;
|
||||
|
||||
public AuthTests(ITestOutputHelper testOutput)
|
||||
{
|
||||
}
|
||||
this.testOutput = testOutput;
|
||||
}
|
||||
|
||||
private static HttpOperationResponse<V1PodList> ExecuteListPods(IKubernetes client)
|
||||
{
|
||||
@@ -35,7 +37,7 @@ namespace k8s.Tests
|
||||
[Fact]
|
||||
public void Anonymous()
|
||||
{
|
||||
using (var server = new MockKubeApiServer(TestOutput))
|
||||
using (var server = new MockKubeApiServer(testOutput))
|
||||
{
|
||||
var client = new Kubernetes(new KubernetesClientConfiguration
|
||||
{
|
||||
@@ -48,7 +50,7 @@ namespace k8s.Tests
|
||||
Assert.Equal(1, listTask.Body.Items.Count);
|
||||
}
|
||||
|
||||
using (var server = new MockKubeApiServer(TestOutput, cxt =>
|
||||
using (var server = new MockKubeApiServer(testOutput, cxt =>
|
||||
{
|
||||
cxt.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
|
||||
return Task.FromResult(false);
|
||||
@@ -71,7 +73,7 @@ namespace k8s.Tests
|
||||
const string testName = "test_name";
|
||||
const string testPassword = "test_password";
|
||||
|
||||
using (var server = new MockKubeApiServer(TestOutput, cxt =>
|
||||
using (var server = new MockKubeApiServer(testOutput, cxt =>
|
||||
{
|
||||
var header = cxt.Request.Headers["Authorization"].FirstOrDefault();
|
||||
|
||||
@@ -182,7 +184,7 @@ namespace k8s.Tests
|
||||
|
||||
var clientCertificateValidationCalled = false;
|
||||
|
||||
using (var server = new MockKubeApiServer(TestOutput, listenConfigure: options =>
|
||||
using (var server = new MockKubeApiServer(testOutput, listenConfigure: options =>
|
||||
{
|
||||
options.UseHttps(new HttpsConnectionAdapterOptions
|
||||
{
|
||||
@@ -264,7 +266,7 @@ namespace k8s.Tests
|
||||
{
|
||||
const string token = "testingtoken";
|
||||
|
||||
using (var server = new MockKubeApiServer(TestOutput, cxt =>
|
||||
using (var server = new MockKubeApiServer(testOutput, cxt =>
|
||||
{
|
||||
var header = cxt.Request.Headers["Authorization"].FirstOrDefault();
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ namespace k8s.Tests
|
||||
public class PodExecTests
|
||||
: WebSocketTestBase
|
||||
{
|
||||
private readonly ITestOutputHelper testOutput;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="KubeApiClient"/> exec-in-pod test suite.
|
||||
/// </summary>
|
||||
@@ -31,6 +33,7 @@ namespace k8s.Tests
|
||||
public PodExecTests(ITestOutputHelper testOutput)
|
||||
: base(testOutput)
|
||||
{
|
||||
this.testOutput = testOutput;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -49,7 +52,7 @@ namespace k8s.Tests
|
||||
|
||||
using (Kubernetes client = CreateTestClient())
|
||||
{
|
||||
Log.LogInformation("Invoking exec operation...");
|
||||
testOutput.WriteLine("Invoking exec operation...");
|
||||
|
||||
WebSocket clientSocket = await client.WebSocketNamespacedPodExecAsync(
|
||||
name: "mypod",
|
||||
@@ -63,19 +66,19 @@ namespace k8s.Tests
|
||||
);
|
||||
Assert.Equal(K8sProtocol.ChannelV1, clientSocket.SubProtocol); // For WebSockets, the Kubernetes API defaults to the binary channel (v1) protocol.
|
||||
|
||||
Log.LogInformation("Client socket connected (socket state is {ClientSocketState}). Waiting for server-side socket to become available...", clientSocket.State);
|
||||
testOutput.WriteLine($"Client socket connected (socket state is {clientSocket.State}). Waiting for server-side socket to become available...");
|
||||
|
||||
WebSocket serverSocket = await WebSocketTestAdapter.AcceptedPodExecV1Connection;
|
||||
Log.LogInformation("Server-side socket is now available (socket state is {ServerSocketState}). Sending data to server socket...", serverSocket.State);
|
||||
testOutput.WriteLine($"Server-side socket is now available (socket state is {serverSocket.State}). Sending data to server socket...");
|
||||
|
||||
const int STDOUT = 1;
|
||||
const string expectedOutput = "This is text send to STDOUT.";
|
||||
|
||||
int bytesSent = await SendMultiplexed(serverSocket, STDOUT, expectedOutput);
|
||||
Log.LogInformation("Sent {ByteCount} bytes to server socket; receiving from client socket...", bytesSent);
|
||||
testOutput.WriteLine($"Sent {bytesSent} bytes to server socket; receiving from client socket...");
|
||||
|
||||
(string receivedText, byte streamIndex, int bytesReceived) = await ReceiveTextMultiplexed(clientSocket);
|
||||
Log.LogInformation("Received {ByteCount} bytes from client socket ('{ReceivedText}', stream {StreamIndex}).", bytesReceived, receivedText, streamIndex);
|
||||
testOutput.WriteLine($"Received {bytesReceived} bytes from client socket ('{receivedText}', stream {streamIndex}).");
|
||||
|
||||
Assert.Equal(STDOUT, streamIndex);
|
||||
Assert.Equal(expectedOutput, receivedText);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.0.0" />
|
||||
<PackageReference Include="System.Reactive" Version="3.1.1" />
|
||||
<PackageReference Include="Nito.AsyncEx" Version="4.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp2.0' ">
|
||||
@@ -19,7 +20,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0-preview-20180221-13" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
|
||||
<PackageReference Include="xunit" Version="2.3.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" NoWarn="NU1701" />
|
||||
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
using k8s.Tests.Logging;
|
||||
using System;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace k8s.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// The base class for test suites.
|
||||
/// </summary>
|
||||
public abstract class TestBase
|
||||
: IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new test-suite.
|
||||
/// </summary>
|
||||
/// <param name="testOutput">
|
||||
/// Output for the current test.
|
||||
/// </param>
|
||||
protected TestBase(ITestOutputHelper testOutput)
|
||||
{
|
||||
if (testOutput == null)
|
||||
throw new ArgumentNullException(nameof(testOutput));
|
||||
|
||||
// We *must* have a synchronisation context for the test, or we'll see random deadlocks.
|
||||
if (SynchronizationContext.Current == null)
|
||||
{
|
||||
SynchronizationContext.SetSynchronizationContext(
|
||||
new SynchronizationContext()
|
||||
);
|
||||
}
|
||||
|
||||
TestOutput = testOutput;
|
||||
LoggerFactory = new LoggerFactory().AddTestOutput(TestOutput, MinLogLevel);
|
||||
Log = LoggerFactory.CreateLogger("CurrentTest");
|
||||
|
||||
// Ugly hack to get access to metadata for the current test.
|
||||
CurrentTest = (ITest)
|
||||
TestOutput.GetType()
|
||||
.GetField("test", BindingFlags.NonPublic | BindingFlags.Instance)
|
||||
.GetValue(TestOutput);
|
||||
|
||||
Assert.True(CurrentTest != null, "Cannot retrieve current test from ITestOutputHelper.");
|
||||
|
||||
Disposal.Add(
|
||||
Log.BeginScope("CurrentTest", CurrentTest.DisplayName)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finaliser for <see cref="TestBase"/>.
|
||||
/// </summary>
|
||||
~TestBase()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose of resources being used by the test suite.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose of resources being used by the test suite.
|
||||
/// </summary>
|
||||
/// <param name="disposing">
|
||||
/// Explicit disposal?
|
||||
/// </param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
try
|
||||
{
|
||||
Disposal.Dispose();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (LoggerFactory is IDisposable loggerFactoryDisposal)
|
||||
loggerFactoryDisposal.Dispose();
|
||||
|
||||
if (Log is IDisposable logDisposal)
|
||||
logDisposal.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="CompositeDisposable"/> representing resources used by the test.
|
||||
/// </summary>
|
||||
protected CompositeDisposable Disposal { get; } = new CompositeDisposable();
|
||||
|
||||
/// <summary>
|
||||
/// Output for the current test.
|
||||
/// </summary>
|
||||
protected ITestOutputHelper TestOutput { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="ITest"/> representing the current test.
|
||||
/// </summary>
|
||||
protected ITest CurrentTest { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The logger for the current test.
|
||||
/// </summary>
|
||||
protected ILogger Log { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The logger factory for the current test.
|
||||
/// </summary>
|
||||
protected ILoggerFactory LoggerFactory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The logging level for the current test.
|
||||
/// </summary>
|
||||
protected virtual LogLevel MinLogLevel => LogLevel.Information;
|
||||
|
||||
/// <summary>
|
||||
/// The test server logging level for the current test.
|
||||
/// </summary>
|
||||
protected virtual LogLevel MinServerLogLevel => LogLevel.Warning;
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,13 @@ using Xunit.Abstractions;
|
||||
|
||||
namespace k8s.Tests
|
||||
{
|
||||
public class V1StatusObjectViewTests
|
||||
: TestBase
|
||||
public class V1StatusObjectViewTests
|
||||
{
|
||||
public V1StatusObjectViewTests(ITestOutputHelper testOutput) : base(testOutput)
|
||||
private readonly ITestOutputHelper testOutput;
|
||||
|
||||
public V1StatusObjectViewTests(ITestOutputHelper testOutput)
|
||||
{
|
||||
this.testOutput = testOutput;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -22,7 +24,7 @@ namespace k8s.Tests
|
||||
Status = "test status"
|
||||
};
|
||||
|
||||
using (var server = new MockKubeApiServer(TestOutput, resp: JsonConvert.SerializeObject(v1Status)))
|
||||
using (var server = new MockKubeApiServer(testOutput, resp: JsonConvert.SerializeObject(v1Status)))
|
||||
{
|
||||
var client = new Kubernetes(new KubernetesClientConfiguration
|
||||
{
|
||||
@@ -52,7 +54,7 @@ namespace k8s.Tests
|
||||
}
|
||||
};
|
||||
|
||||
using (var server = new MockKubeApiServer(TestOutput, resp: JsonConvert.SerializeObject(corev1Namespace)))
|
||||
using (var server = new MockKubeApiServer(testOutput, resp: JsonConvert.SerializeObject(corev1Namespace)))
|
||||
{
|
||||
var client = new Kubernetes(new KubernetesClientConfiguration
|
||||
{
|
||||
|
||||
@@ -13,22 +13,26 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Nito.AsyncEx;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace k8s.Tests
|
||||
{
|
||||
public class WatchTests
|
||||
: TestBase
|
||||
{
|
||||
private static readonly string MockAddedEventStreamLine = BuildWatchEventStreamLine(WatchEventType.Added);
|
||||
private static readonly string MockDeletedStreamLine = BuildWatchEventStreamLine(WatchEventType.Deleted);
|
||||
private static readonly string MockModifiedStreamLine = BuildWatchEventStreamLine(WatchEventType.Modified);
|
||||
private static readonly string MockErrorStreamLine = BuildWatchEventStreamLine(WatchEventType.Error);
|
||||
private static readonly string MockBadStreamLine = "bad json";
|
||||
private static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(15);
|
||||
|
||||
public WatchTests(ITestOutputHelper testOutput) : base(testOutput)
|
||||
private readonly ITestOutputHelper testOutput;
|
||||
|
||||
public WatchTests(ITestOutputHelper testOutput)
|
||||
{
|
||||
this.testOutput = testOutput;
|
||||
}
|
||||
|
||||
private static string BuildWatchEventStreamLine(WatchEventType eventType)
|
||||
@@ -50,9 +54,9 @@ namespace k8s.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CannotWatch()
|
||||
public async Task CannotWatch()
|
||||
{
|
||||
using (var server = new MockKubeApiServer(testOutput: TestOutput))
|
||||
using (var server = new MockKubeApiServer(testOutput: testOutput))
|
||||
{
|
||||
var client = new Kubernetes(new KubernetesClientConfiguration
|
||||
{
|
||||
@@ -60,53 +64,54 @@ namespace k8s.Tests
|
||||
});
|
||||
|
||||
// did not pass watch param
|
||||
var listTask = await client.ListNamespacedPodWithHttpMessagesAsync("default");
|
||||
Assert.ThrowsAny<KubernetesClientException>(() =>
|
||||
{
|
||||
var listTask = client.ListNamespacedPodWithHttpMessagesAsync("default").Result;
|
||||
Assert.ThrowsAny<KubernetesClientException>(() =>
|
||||
{
|
||||
listTask.Watch<V1Pod>((type, item) => { });
|
||||
});
|
||||
}
|
||||
listTask.Watch<V1Pod>((type, item) => { });
|
||||
});
|
||||
|
||||
// server did not response line by line
|
||||
await Assert.ThrowsAnyAsync<Exception>(() =>
|
||||
{
|
||||
Assert.ThrowsAny<Exception>(() =>
|
||||
{
|
||||
var listTask = client.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).Result;
|
||||
return client.ListNamespacedPodWithHttpMessagesAsync("default", watch: true);
|
||||
|
||||
// this line did not throw
|
||||
// listTask.Watch<Corev1Pod>((type, item) => { });
|
||||
});
|
||||
}
|
||||
// this line did not throw
|
||||
// listTask.Watch<Corev1Pod>((type, item) => { });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SuriveBadLine()
|
||||
public async Task SuriveBadLine()
|
||||
{
|
||||
using (CountdownEvent eventsReceived = new CountdownEvent(4 /* first line of response is eaten by WatcherDelegatingHandler */))
|
||||
using (var server = new MockKubeApiServer(TestOutput, async httpContext =>
|
||||
{
|
||||
httpContext.Response.StatusCode = (int) HttpStatusCode.OK;
|
||||
httpContext.Response.ContentLength = null;
|
||||
AsyncCountdownEvent eventsReceived = new AsyncCountdownEvent(4 /* first line of response is eaten by WatcherDelegatingHandler */);
|
||||
AsyncManualResetEvent serverShutdown = new AsyncManualResetEvent();
|
||||
|
||||
await WriteStreamLine(httpContext, MockKubeApiServer.MockPodResponse);
|
||||
await WriteStreamLine(httpContext, MockBadStreamLine);
|
||||
await WriteStreamLine(httpContext, MockAddedEventStreamLine);
|
||||
await WriteStreamLine(httpContext, MockBadStreamLine);
|
||||
await WriteStreamLine(httpContext, MockModifiedStreamLine);
|
||||
using (var server =
|
||||
new MockKubeApiServer(
|
||||
testOutput,
|
||||
async httpContext =>
|
||||
{
|
||||
httpContext.Response.StatusCode = (int)HttpStatusCode.OK;
|
||||
httpContext.Response.ContentLength = null;
|
||||
|
||||
// make server alive, cannot set to int.max as of it would block response
|
||||
await Task.Delay(TimeSpan.FromDays(1));
|
||||
return false;
|
||||
}))
|
||||
await WriteStreamLine(httpContext, MockKubeApiServer.MockPodResponse);
|
||||
await WriteStreamLine(httpContext, MockBadStreamLine);
|
||||
await WriteStreamLine(httpContext, MockAddedEventStreamLine);
|
||||
await WriteStreamLine(httpContext, MockBadStreamLine);
|
||||
await WriteStreamLine(httpContext, MockModifiedStreamLine);
|
||||
|
||||
// make server alive, cannot set to int.max as of it would block response
|
||||
await serverShutdown.WaitAsync();
|
||||
return false;
|
||||
}))
|
||||
{
|
||||
var client = new Kubernetes(new KubernetesClientConfiguration
|
||||
{
|
||||
Host = server.Uri.ToString()
|
||||
});
|
||||
|
||||
var listTask = client.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).Result;
|
||||
var listTask = await client.ListNamespacedPodWithHttpMessagesAsync("default", watch: true);
|
||||
|
||||
var events = new HashSet<WatchEventType>();
|
||||
var errors = 0;
|
||||
@@ -114,14 +119,14 @@ namespace k8s.Tests
|
||||
var watcher = listTask.Watch<V1Pod>(
|
||||
(type, item) =>
|
||||
{
|
||||
Log.LogInformation("Watcher received '{EventType}' event.", type);
|
||||
testOutput.WriteLine($"Watcher received '{type}' event.");
|
||||
|
||||
events.Add(type);
|
||||
eventsReceived.Signal();
|
||||
},
|
||||
error =>
|
||||
{
|
||||
Log.LogInformation("Watcher received '{ErrorType}' error.", error.GetType().FullName);
|
||||
testOutput.WriteLine($"Watcher received '{error.GetType().FullName}' error.");
|
||||
|
||||
errors += 1;
|
||||
eventsReceived.Signal();
|
||||
@@ -129,8 +134,10 @@ namespace k8s.Tests
|
||||
);
|
||||
|
||||
// wait server yields all events
|
||||
await Task.WhenAny(eventsReceived.WaitAsync(), Task.Delay(TestTimeout));
|
||||
|
||||
Assert.True(
|
||||
eventsReceived.Wait(TimeSpan.FromMilliseconds(3000)),
|
||||
eventsReceived.CurrentCount == 0,
|
||||
"Timed out waiting for all events / errors to be received."
|
||||
);
|
||||
|
||||
@@ -141,23 +148,27 @@ namespace k8s.Tests
|
||||
|
||||
Assert.True(watcher.Watching);
|
||||
|
||||
// prevent from server down exception trigger
|
||||
Thread.Sleep(TimeSpan.FromMilliseconds(1000));
|
||||
// Let the server know it can initiate a shut down.
|
||||
serverShutdown.Set();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisposeWatch()
|
||||
public async Task DisposeWatch()
|
||||
{
|
||||
using (var eventsReceived = new CountdownEvent(1))
|
||||
using (var server = new MockKubeApiServer(TestOutput, async httpContext =>
|
||||
var eventsReceived = new AsyncCountdownEvent(1);
|
||||
bool serverRunning = true;
|
||||
|
||||
using (var server = new MockKubeApiServer(testOutput, async httpContext =>
|
||||
{
|
||||
await WriteStreamLine(httpContext, MockKubeApiServer.MockPodResponse);
|
||||
|
||||
for (;;)
|
||||
while (serverRunning)
|
||||
{
|
||||
await WriteStreamLine(httpContext, MockAddedEventStreamLine);
|
||||
}
|
||||
|
||||
return true;
|
||||
}))
|
||||
{
|
||||
var client = new Kubernetes(new KubernetesClientConfiguration
|
||||
@@ -165,21 +176,22 @@ namespace k8s.Tests
|
||||
Host = server.Uri.ToString()
|
||||
});
|
||||
|
||||
var listTask = client.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).Result;
|
||||
|
||||
var listTask = await client.ListNamespacedPodWithHttpMessagesAsync("default", watch: true);
|
||||
|
||||
var events = new HashSet<WatchEventType>();
|
||||
|
||||
var watcher = listTask.Watch<V1Pod>(
|
||||
(type, item) => {
|
||||
(type, item) =>
|
||||
{
|
||||
events.Add(type);
|
||||
eventsReceived.Signal();
|
||||
}
|
||||
);
|
||||
|
||||
// wait at least an event
|
||||
await Task.WhenAny(eventsReceived.WaitAsync(), Task.Delay(TestTimeout));
|
||||
Assert.True(
|
||||
eventsReceived.Wait(TimeSpan.FromSeconds(10)),
|
||||
eventsReceived.CurrentCount == 0,
|
||||
"Timed out waiting for events."
|
||||
);
|
||||
|
||||
@@ -190,19 +202,28 @@ namespace k8s.Tests
|
||||
|
||||
events.Clear();
|
||||
|
||||
// make sure wait event called
|
||||
Thread.Sleep(TimeSpan.FromMilliseconds(1000));
|
||||
// Let the server disconnect
|
||||
serverRunning = false;
|
||||
|
||||
var timeout = Task.Delay(TestTimeout);
|
||||
|
||||
while(!timeout.IsCompleted && watcher.Watching)
|
||||
{
|
||||
await Task.Yield();
|
||||
}
|
||||
|
||||
Assert.Empty(events);
|
||||
Assert.False(watcher.Watching);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WatchAllEvents()
|
||||
public async Task WatchAllEvents()
|
||||
{
|
||||
using (CountdownEvent eventsReceived = new CountdownEvent(4 /* first line of response is eaten by WatcherDelegatingHandler */))
|
||||
using (var server = new MockKubeApiServer(TestOutput, async httpContext =>
|
||||
AsyncCountdownEvent eventsReceived = new AsyncCountdownEvent(4 /* first line of response is eaten by WatcherDelegatingHandler */);
|
||||
AsyncManualResetEvent serverShutdown = new AsyncManualResetEvent();
|
||||
|
||||
using (var server = new MockKubeApiServer(testOutput, async httpContext =>
|
||||
{
|
||||
await WriteStreamLine(httpContext, MockKubeApiServer.MockPodResponse);
|
||||
await WriteStreamLine(httpContext, MockAddedEventStreamLine);
|
||||
@@ -211,7 +232,7 @@ namespace k8s.Tests
|
||||
await WriteStreamLine(httpContext, MockErrorStreamLine);
|
||||
|
||||
// make server alive, cannot set to int.max as of it would block response
|
||||
await Task.Delay(TimeSpan.FromDays(1));
|
||||
await serverShutdown.WaitAsync();
|
||||
return false;
|
||||
}))
|
||||
{
|
||||
@@ -229,14 +250,14 @@ namespace k8s.Tests
|
||||
var watcher = listTask.Watch<V1Pod>(
|
||||
(type, item) =>
|
||||
{
|
||||
Log.LogInformation("Watcher received '{EventType}' event.", type);
|
||||
testOutput.WriteLine($"Watcher received '{type}' event.");
|
||||
|
||||
events.Add(type);
|
||||
eventsReceived.Signal();
|
||||
},
|
||||
error =>
|
||||
{
|
||||
Log.LogInformation("Watcher received '{ErrorType}' error.", error.GetType().FullName);
|
||||
testOutput.WriteLine($"Watcher received '{error.GetType().FullName}' error.");
|
||||
|
||||
errors += 1;
|
||||
eventsReceived.Signal();
|
||||
@@ -244,8 +265,10 @@ namespace k8s.Tests
|
||||
);
|
||||
|
||||
// wait server yields all events
|
||||
await Task.WhenAny(eventsReceived.WaitAsync(), Task.Delay(TestTimeout));
|
||||
|
||||
Assert.True(
|
||||
eventsReceived.Wait(TimeSpan.FromMilliseconds(3000)),
|
||||
eventsReceived.CurrentCount == 0,
|
||||
"Timed out waiting for all events / errors to be received."
|
||||
);
|
||||
|
||||
@@ -254,23 +277,24 @@ namespace k8s.Tests
|
||||
Assert.Contains(WatchEventType.Modified, events);
|
||||
Assert.Contains(WatchEventType.Error, events);
|
||||
|
||||
|
||||
Assert.Equal(0, errors);
|
||||
|
||||
Assert.True(watcher.Watching);
|
||||
|
||||
serverShutdown.Set();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WatchServerDisconnect()
|
||||
public async Task WatchServerDisconnect()
|
||||
{
|
||||
Exception exceptionCatched = null;
|
||||
using (var exceptionReceived = new AutoResetEvent(false))
|
||||
using (var waitForException = new AutoResetEvent(false))
|
||||
using (var server = new MockKubeApiServer(TestOutput, async httpContext =>
|
||||
var exceptionReceived = new AsyncManualResetEvent(false);
|
||||
var waitForException = new AsyncManualResetEvent(false);
|
||||
using (var server = new MockKubeApiServer(testOutput, async httpContext =>
|
||||
{
|
||||
await WriteStreamLine(httpContext, MockKubeApiServer.MockPodResponse);
|
||||
waitForException.WaitOne();
|
||||
await waitForException.WaitAsync();
|
||||
throw new IOException("server down");
|
||||
}))
|
||||
{
|
||||
@@ -279,20 +303,23 @@ namespace k8s.Tests
|
||||
Host = server.Uri.ToString()
|
||||
});
|
||||
|
||||
var listTask = client.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).Result;
|
||||
var listTask = await client.ListNamespacedPodWithHttpMessagesAsync("default", watch: true);
|
||||
|
||||
waitForException.Set();
|
||||
Watcher<V1Pod> watcher;
|
||||
watcher = listTask.Watch<V1Pod>(
|
||||
(type, item) => { },
|
||||
e => {
|
||||
e =>
|
||||
{
|
||||
exceptionCatched = e;
|
||||
exceptionReceived.Set();
|
||||
});
|
||||
|
||||
// wait server down
|
||||
await Task.WhenAny(exceptionReceived.WaitAsync(), Task.Delay(TestTimeout));
|
||||
|
||||
Assert.True(
|
||||
exceptionReceived.WaitOne(TimeSpan.FromSeconds(10)),
|
||||
exceptionReceived.IsSet,
|
||||
"Timed out waiting for exception"
|
||||
);
|
||||
|
||||
@@ -314,16 +341,18 @@ namespace k8s.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestWatchWithHandlers()
|
||||
public async Task TestWatchWithHandlers()
|
||||
{
|
||||
using (CountdownEvent eventsReceived = new CountdownEvent(1))
|
||||
using (var server = new MockKubeApiServer(TestOutput, async httpContext =>
|
||||
AsyncCountdownEvent eventsReceived = new AsyncCountdownEvent(1);
|
||||
AsyncManualResetEvent serverShutdown = new AsyncManualResetEvent();
|
||||
|
||||
using (var server = new MockKubeApiServer(testOutput, async httpContext =>
|
||||
{
|
||||
await WriteStreamLine(httpContext, MockKubeApiServer.MockPodResponse);
|
||||
await WriteStreamLine(httpContext, MockAddedEventStreamLine);
|
||||
|
||||
// make server alive, cannot set to int.max as of it would block response
|
||||
await Task.Delay(TimeSpan.FromDays(1));
|
||||
await serverShutdown.WaitAsync();
|
||||
return false;
|
||||
}))
|
||||
{
|
||||
@@ -338,20 +367,23 @@ namespace k8s.Tests
|
||||
Assert.False(handler1.Called);
|
||||
Assert.False(handler2.Called);
|
||||
|
||||
var listTask = client.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).Result;
|
||||
var listTask = await client.ListNamespacedPodWithHttpMessagesAsync("default", watch: true);
|
||||
|
||||
var events = new HashSet<WatchEventType>();
|
||||
|
||||
var watcher = listTask.Watch<V1Pod>(
|
||||
(type, item) => {
|
||||
(type, item) =>
|
||||
{
|
||||
events.Add(type);
|
||||
eventsReceived.Signal();
|
||||
}
|
||||
);
|
||||
|
||||
// wait server yields all events
|
||||
await Task.WhenAny(eventsReceived.WaitAsync(), Task.Delay(TestTimeout));
|
||||
|
||||
Assert.True(
|
||||
eventsReceived.Wait(TimeSpan.FromMilliseconds(10000)),
|
||||
eventsReceived.CurrentCount == 0,
|
||||
"Timed out waiting for all events / errors to be received."
|
||||
);
|
||||
|
||||
@@ -359,6 +391,8 @@ namespace k8s.Tests
|
||||
|
||||
Assert.True(handler1.Called);
|
||||
Assert.True(handler2.Called);
|
||||
|
||||
serverShutdown.Set();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,14 +19,15 @@ namespace k8s.Tests
|
||||
/// <summary>
|
||||
/// The base class for Kubernetes WebSocket test suites.
|
||||
/// </summary>
|
||||
public abstract class WebSocketTestBase
|
||||
: TestBase
|
||||
public abstract class WebSocketTestBase : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The next server port to use.
|
||||
/// </summary>
|
||||
static int NextPort = 13255;
|
||||
|
||||
private readonly ITestOutputHelper testOutput;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="WebSocketTestBase"/>.
|
||||
/// </summary>
|
||||
@@ -34,13 +35,14 @@ namespace k8s.Tests
|
||||
/// Output for the current test.
|
||||
/// </param>
|
||||
protected WebSocketTestBase(ITestOutputHelper testOutput)
|
||||
: base(testOutput)
|
||||
{
|
||||
this.testOutput = testOutput;
|
||||
|
||||
int port = Interlocked.Increment(ref NextPort);
|
||||
|
||||
// Useful to diagnose test timeouts.
|
||||
TestCancellation.Register(
|
||||
() => Log.LogInformation("Test-level cancellation token has been canceled.")
|
||||
() => testOutput.WriteLine("Test-level cancellation token has been canceled.")
|
||||
);
|
||||
|
||||
ServerBaseAddress = new Uri($"http://localhost:{port}");
|
||||
@@ -52,9 +54,6 @@ namespace k8s.Tests
|
||||
.ConfigureLogging(ConfigureTestServerLogging)
|
||||
.UseUrls(ServerBaseAddress.AbsoluteUri)
|
||||
.Build();
|
||||
|
||||
Disposal.Add(CancellationSource);
|
||||
Disposal.Add(Host);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -115,7 +114,7 @@ namespace k8s.Tests
|
||||
throw new ArgumentNullException(nameof(logging));
|
||||
|
||||
logging.ClearProviders(); // Don't log to console.
|
||||
logging.AddTestOutput(TestOutput, MinLogLevel);
|
||||
logging.AddTestOutput(this.testOutput, LogLevel.Information);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -165,7 +164,7 @@ namespace k8s.Tests
|
||||
if (serverSocket == null)
|
||||
throw new ArgumentNullException(nameof(serverSocket));
|
||||
|
||||
Log.LogInformation("Disconnecting...");
|
||||
testOutput.WriteLine("Disconnecting...");
|
||||
|
||||
// Asynchronously perform the server's half of the handshake (the call to clientSocket.CloseAsync will block until it receives the server-side response).
|
||||
ArraySegment<byte> receiveBuffer = new byte[1024];
|
||||
@@ -173,19 +172,16 @@ namespace k8s.Tests
|
||||
.ContinueWith(async received =>
|
||||
{
|
||||
if (received.IsFaulted)
|
||||
Log.LogError(new EventId(0), received.Exception.Flatten().InnerExceptions[0], "Server socket operation to receive Close message failed.");
|
||||
testOutput.WriteLine("Server socket operation to receive Close message failed: {0}", received.Exception.Flatten().InnerExceptions[0]);
|
||||
else if (received.IsCanceled)
|
||||
Log.LogWarning("Server socket operation to receive Close message was canceled.");
|
||||
testOutput.WriteLine("Server socket operation to receive Close message was canceled.");
|
||||
else
|
||||
{
|
||||
Log.LogInformation("Received {MessageType} message from server socket (expecting {ExpectedMessageType}).",
|
||||
received.Result.MessageType,
|
||||
WebSocketMessageType.Close
|
||||
);
|
||||
testOutput.WriteLine($"Received {received.Result.MessageType} message from server socket (expecting {WebSocketMessageType.Close}).");
|
||||
|
||||
if (received.Result.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
Log.LogInformation("Closing server socket (with status {CloseStatus})...", received.Result.CloseStatus);
|
||||
testOutput.WriteLine($"Closing server socket (with status {received.Result.CloseStatus})...");
|
||||
|
||||
await serverSocket.CloseAsync(
|
||||
received.Result.CloseStatus.Value,
|
||||
@@ -193,22 +189,22 @@ namespace k8s.Tests
|
||||
TestCancellation
|
||||
);
|
||||
|
||||
Log.LogInformation("Server socket closed.");
|
||||
testOutput.WriteLine("Server socket closed.");
|
||||
}
|
||||
|
||||
Assert.Equal(WebSocketMessageType.Close, received.Result.MessageType);
|
||||
}
|
||||
});
|
||||
|
||||
Log.LogInformation("Closing client socket...");
|
||||
testOutput.WriteLine("Closing client socket...");
|
||||
|
||||
await clientSocket.CloseAsync(closeStatus, closeStatusDescription, TestCancellation).ConfigureAwait(false);
|
||||
|
||||
Log.LogInformation("Client socket closed.");
|
||||
testOutput.WriteLine("Client socket closed.");
|
||||
|
||||
await closeServerSocket.ConfigureAwait(false);
|
||||
|
||||
Log.LogInformation("Disconnected.");
|
||||
testOutput.WriteLine("Disconnected.");
|
||||
|
||||
Assert.Equal(closeStatus, clientSocket.CloseStatus);
|
||||
Assert.Equal(clientSocket.CloseStatus, serverSocket.CloseStatus);
|
||||
@@ -299,6 +295,12 @@ namespace k8s.Tests
|
||||
);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.CancellationSource.Dispose();
|
||||
this.Host.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="ServiceClientCredentials"/> implementation representing no credentials (i.e. anonymous).
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user