Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions src/DiffEngine.Tests/HeldPort.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/// <summary>
/// A port bound and not listening, for a test that needs one nobody is on. Bound rather than found
/// free and let go, so that nothing else is handed it while the test is using it: a test beside
/// this one binding port zero could otherwise be given the same port, and be told whatever this
/// test had the listener table say about it (<see cref="ListenerTable.Answer"/>), or be found
/// listening there by a test asking whether anybody is.
/// </summary>
sealed class HeldPort : IDisposable
{
readonly Socket socket;

public HeldPort() :
this(IPAddress.Loopback, 0)
{
}

HeldPort(IPAddress address, int port)
{
socket = new(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
try
{
socket.Bind(new IPEndPoint(address, port));
}
catch
{
socket.Dispose();
throw;
}

Port = ((IPEndPoint) socket.LocalEndPoint!).Port;
}

/// <summary>
/// A listener of the test's own on <paramref name="address"/>, asked about by
/// <paramref name="listening"/> while it listens and by <paramref name="stopped"/> once it has
/// stopped, with the port held again in between. The tests beside one bind port zero, and
/// Windows hands a port straight back out: asked about while free, a port was found listened
/// on by one of them within milliseconds. Where one had it first, this starts again on another.
/// </summary>
public static async Task AfterListening(IPAddress address, Func<int, Task> listening, Func<int, Task> stopped)
{
for (var attempt = 0; attempt < 10; attempt++)
{
var listener = new TcpListener(address, 0);
listener.Start();
var port = ((IPEndPoint) listener.LocalEndpoint).Port;
try
{
await listening(port);
}
finally
{
listener.Stop();
}

using var held = TryAgain(address, port);
if (held is null)
{
continue;
}

await stopped(port);
return;
}

throw new("Every port was handed to something else before it could be held again.");
}

static HeldPort? TryAgain(IPAddress address, int port)
{
try
{
return new(address, port);
}
catch (SocketException)
{
return null;
}
}

public int Port { get; }

public void Dispose() =>
socket.Dispose();
}
80 changes: 49 additions & 31 deletions src/DiffEngine.Tests/ListenerTableTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,53 @@ public class ListenerTableTests
/// running is asked of it there.
/// </summary>
[Test]
public async Task AListenerIsListedOnEveryPlatform()
public Task AListenerIsListedOnEveryPlatform() =>
HeldPort.AfterListening(
IPAddress.Loopback,
async _ =>
{
await Assert.That(ListenerTable.IsListed(_)).IsTrue();
await Assert.That(ListenerTable.Enumerated(_)).IsTrue();
},
async _ => await Assert.That(ListenerTable.IsListed(_)).IsFalse());

/// <summary>
/// Answers that overlap and end in either order leave the table answering for both ports,
/// which is what a lookup each test wrapped around the one before did not: the second test to
/// finish put back the first one's.
/// </summary>
[Test]
public async Task AnswersThatOverlapLeaveNothingBehind()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint) listener.LocalEndpoint).Port;
bool listed;
bool enumerated;
try
{
listed = ListenerTable.IsListed(port);
enumerated = ListenerTable.Enumerated(port);
}
finally
{
listener.Stop();
}
using var first = new HeldPort();
using var second = new HeldPort();

await Assert.That(listed).IsTrue();
await Assert.That(enumerated).IsTrue();
await Assert.That(ListenerTable.IsListed(port)).IsFalse();
var firstAnswer = ListenerTable.Answer(first.Port, () => true);
var secondAnswer = ListenerTable.Answer(second.Port, () => true);
await Assert.That(ListenerTable.IsListed(first.Port)).IsTrue();
await Assert.That(ListenerTable.IsListed(second.Port)).IsTrue();

firstAnswer.Dispose();
await Assert.That(ListenerTable.IsListed(first.Port)).IsFalse();
await Assert.That(ListenerTable.IsListed(second.Port)).IsTrue();

secondAnswer.Dispose();
await Assert.That(ListenerTable.IsListed(second.Port)).IsFalse();
}

/// <summary>
/// One answer a port, so two tests that believe they hold the same port find out, rather than
/// one quietly answering for the other.
/// </summary>
[Test]
public async Task APortIsAnsweredForOnce()
{
using var held = new HeldPort();
using var answer = ListenerTable.Answer(held.Port, () => true);

await Assert.That(() => ListenerTable.Answer(held.Port, () => false))
.Throws<InvalidOperationException>();
await Assert.That(ListenerTable.IsListed(held.Port)).IsTrue();
}

[Test]
Expand Down Expand Up @@ -96,19 +123,10 @@ public async Task AListenerSaysWhoseItIs()
IPAddress[] addresses = Socket.OSSupportsIPv6 ? [IPAddress.Loopback, IPAddress.IPv6Loopback] : [IPAddress.Loopback];
foreach (var address in addresses)
{
var listener = new TcpListener(address, 0);
listener.Start();
var port = ((IPEndPoint) listener.LocalEndpoint).Port;
try
{
await Assert.That(ListenerTable.Owner(port)).IsEqualTo(current.Id);
}
finally
{
listener.Stop();
}

await Assert.That(ListenerTable.Owner(port)).IsNull();
await HeldPort.AfterListening(
address,
async _ => await Assert.That(ListenerTable.Owner(_)).IsEqualTo(current.Id),
async _ => await Assert.That(ListenerTable.Owner(_)).IsNull());
}
}

Expand Down
35 changes: 10 additions & 25 deletions src/DiffEngine.Tests/TrayDetectorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,11 @@ public class TrayDetectorTests
{
[Test]
[RunOn(TUnit.Core.Enums.OS.Linux | TUnit.Core.Enums.OS.MacOs)]
public async Task AListenerOnThePiperPortIsATray()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint) listener.LocalEndpoint).Port;
bool running;
try
{
running = TrayDetector.IsRunning(port);
}
finally
{
listener.Stop();
}

await Assert.That(running).IsTrue();
await Assert.That(TrayDetector.IsRunning(port)).IsFalse();
}
public Task AListenerOnThePiperPortIsATray() =>
HeldPort.AfterListening(
IPAddress.Loopback,
async _ => await Assert.That(TrayDetector.IsRunning(_)).IsTrue(),
async _ => await Assert.That(TrayDetector.IsRunning(_)).IsFalse());

/// <summary>
/// No tray, where the piper's send would have taken it as one and let the connect decide:
Expand All @@ -38,20 +25,18 @@ public async Task ATableThatCannotBeReadIsNoTray()
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint) listener.LocalEndpoint).Port;
var previous = ListenerTable.Lookup;
// Every other port goes through untouched, since the tests beside this one are asking
// about theirs at the same time
ListenerTable.Lookup = _ => _ == port ? throw new("The table could not be read.") : previous(_);
bool running;
bool held;
try
{
running = TrayDetector.IsRunning(port);
held = ListenerTable.IsHeld(port);
using (ListenerTable.Answer(port, () => throw new("The table could not be read.")))
{
running = TrayDetector.IsRunning(port);
held = ListenerTable.IsHeld(port);
}
}
finally
{
ListenerTable.Lookup = previous;
listener.Stop();
}

Expand Down
38 changes: 20 additions & 18 deletions src/DiffEngine.Tests/ViewerClientUnownedTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,13 +168,16 @@ public async Task AnOwnerStartedLaterIsFoundByTheTellingSends()
public async Task APortAConnectFoundEmptyIsNotAskedAboutAgainSoSoon()
{
ViewerClient.RecheckUnlistedAfter = TimeSpan.Zero;
var port = FreePort();
var held = new HeldPort();
var port = held.Port;
using var unreadable = new Lookup(port)
{
Unreadable = true
};
await Assert.That(ViewerClient.TrySend(settle, out _, port, skipIfUnowned: true)).IsFalse();

// Let go only to be listened on, so the port is held again a moment later
held.Dispose();
using var owner = new Owner(port);
await Assert.That(ViewerClient.TrySend(settle, out _, port, skipIfUnowned: true)).IsFalse();
await Assert.That(owner.Heard).IsEmpty();
Expand Down Expand Up @@ -303,7 +306,8 @@ public async Task APortNobodyHoldsIsNotProbed()
public async Task ATableThatCannotBeReadLeavesTheConnectToAnswer()
{
using var owner = new Owner();
var dead = FreePort();
using var held = new HeldPort();
var dead = held.Port;
using var unreadable = new Lookup(owner.Port, dead)
{
Unreadable = true
Expand Down Expand Up @@ -457,19 +461,17 @@ await Assert.That(await ViewerClient.SendAsync(settle, Cancel.None, 0, skipIfUno
/// Stands in front of the listener table for the ports a test names, counting how often each
/// was asked about and, when told to, failing the way a table that cannot be read does, or
/// saying nobody listens there whoever does. Every other port goes through untouched, since
/// the tests beside this one are asking about theirs at the same time.
/// the tests beside this one are asking about theirs at the same time. Each port named is one
/// the test holds (<see cref="HeldPort"/> or an <see cref="Owner"/>), or port zero, which
/// nothing is ever handed.
/// </summary>
sealed class Lookup : IDisposable
{
readonly Func<int, bool> previous = ListenerTable.Lookup;
readonly int[] ports;
readonly IDisposable[] answers;
int asked;

public Lookup(params int[] ports)
{
this.ports = ports;
ListenerTable.Lookup = Answer;
}
public Lookup(params int[] ports) =>
answers = ports.Select(_ => ListenerTable.Answer(_, () => Answer(_))).ToArray();

public bool Unreadable { get; init; }

Expand All @@ -479,11 +481,6 @@ public Lookup(params int[] ports)

bool Answer(int port)
{
if (!ports.Contains(port))
{
return previous(port);
}

Interlocked.Increment(ref asked);
if (Unreadable)
{
Expand All @@ -495,11 +492,16 @@ bool Answer(int port)
return false;
}

return previous(port);
return ListenerTable.Listed(port);
}

public void Dispose() =>
ListenerTable.Lookup = previous;
public void Dispose()
{
foreach (var answer in answers)
{
answer.Dispose();
}
}
}

/// <summary>
Expand Down
9 changes: 3 additions & 6 deletions src/DiffEngine.Tests/ViewerProtocolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1455,9 +1455,8 @@ await Assert.That(await ViewerClient.SendAsync(new(ViewerVerb.List), default, po
[NotInParallel]
public async Task AConnectGivenUpOnIsObserved()
{
ViewerServer.TryBind(0, out var server);
var port = server!.Port;
server.Dispose();
using var held = new HeldPort();
var port = held.Port;
ViewerClient.ForgetUnowned();

var unobserved = new List<Exception>();
Expand All @@ -1469,8 +1468,7 @@ void Record(object? sender, UnobservedTaskExceptionEventArgs args)
}
}

var lookup = ListenerTable.Lookup;
ListenerTable.Lookup = _ => _ == port || lookup(_);
using var listed = ListenerTable.Answer(port, () => true);
TaskScheduler.UnobservedTaskException += Record;
try
{
Expand All @@ -1491,7 +1489,6 @@ void Record(object? sender, UnobservedTaskExceptionEventArgs args)
finally
{
TaskScheduler.UnobservedTaskException -= Record;
ListenerTable.Lookup = lookup;
ViewerClient.ForgetUnowned();
}

Expand Down
Loading
Loading