From 3df626f09843b8ed9e0d98cd9df0e1bd9f87ffc5 Mon Sep 17 00:00:00 2001 From: Filippo Banno Date: Fri, 24 Jan 2020 16:22:37 +0000 Subject: [PATCH] Refactored transport tests into their own class Reduced a bit the number of equivalent tests run. --- .../ITransportBuilder.cs | 206 ++++++++++++++ .../PeerDiscoveryTest.cs | 262 ++++-------------- .../TransportTest.cs | 171 ++++++++++++ .../Matchmaking.PeerDiscovery.Test/Utils.cs | 8 +- 4 files changed, 437 insertions(+), 210 deletions(-) create mode 100644 libs/Matchmaking/test/Matchmaking.PeerDiscovery.Test/ITransportBuilder.cs create mode 100644 libs/Matchmaking/test/Matchmaking.PeerDiscovery.Test/TransportTest.cs diff --git a/libs/Matchmaking/test/Matchmaking.PeerDiscovery.Test/ITransportBuilder.cs b/libs/Matchmaking/test/Matchmaking.PeerDiscovery.Test/ITransportBuilder.cs new file mode 100644 index 0000000..9f61d11 --- /dev/null +++ b/libs/Matchmaking/test/Matchmaking.PeerDiscovery.Test/ITransportBuilder.cs @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace Microsoft.MixedReality.Sharing.Matchmaking.Test +{ + // Utility for making multiple instances of IPeerDiscoveryTransport communicating with each other. + internal interface ITransportBuilder + { + IPeerDiscoveryTransport MakeTransport(int userIndex); + bool SimulatesPacketLoss { get; } + } + + internal class MemoryTransportBuilder : ITransportBuilder + { + public IPeerDiscoveryTransport MakeTransport(int userIndex) + { + return new MemoryPeerDiscoveryTransport(userIndex); + } + + public bool SimulatesPacketLoss => false; + } + + internal class UdpTransportBuilder : ITransportBuilder + { + private readonly ushort port_ = Utils.NewPortNumber.Calculate(); + + public IPeerDiscoveryTransport MakeTransport(int userIndex) + { + return new UdpPeerDiscoveryTransport(new IPAddress(0xffffff7f), port_, new IPAddress(0x0000007f + (userIndex << 24))); + } + public bool SimulatesPacketLoss => false; + } + + internal class UdpMulticastTransportBuilder : ITransportBuilder + { + private readonly ushort port_ = Utils.NewPortNumber.Calculate(); + + public IPeerDiscoveryTransport MakeTransport(int userIndex) + { + return new UdpPeerDiscoveryTransport(new IPAddress(0x000000e0), port_, new IPAddress(0x0000007f + (userIndex << 24))); + } + public bool SimulatesPacketLoss => false; + } + + internal class UdpReorderedTransportBuilder : ITransportBuilder, IDisposable + { + public const int MaxDelayMs = 25; + public const int MaxRetries = 3; + + private readonly Socket relay_; + private readonly ushort port_ = Utils.NewPortNumber.Calculate(); + private readonly List recipients_ = new List(); + + private readonly Random random_; + + // Wraps a packet for use in a map. + private class Packet + { + public IPEndPoint EndPoint; + public byte[] Contents; + + public Packet(IPEndPoint endPoint, ArraySegment contents) + { + EndPoint = endPoint; + Contents = new byte[contents.Count]; + for (int i = 0; i < contents.Count; ++i) + { + Contents[i] = contents[i]; + } + } + + public override bool Equals(object other) + { + if (other is Packet rhs) + { + return EndPoint.Equals(rhs.EndPoint) && Contents.SequenceEqual(rhs.Contents); + } + return false; + } + + public override int GetHashCode() + { + // Not a great hash but it shouldn't matter in this case. + var result = 0; + foreach (byte b in Contents) + { + result = (result * 31) ^ b; + } + return EndPoint.GetHashCode() ^ result; + } + } + + // Keeps track of each packet that goes through the relay and counts its repetitions. + // See receive loop for usage. + private readonly Dictionary packetCounters; + + public bool SimulatesPacketLoss => (packetCounters != null); + public IPeerDiscoveryTransport MakeTransport(int userIndex) + { + // Peers all send packets to the relay. + var address = new IPAddress(0x0000007f + (userIndex << 24)); + lock (recipients_) + { + var endpoint = new IPEndPoint(address, port_); + + // Remove first so the same agent can be re-created multiple times + recipients_.Remove(endpoint); + recipients_.Add(endpoint); + } + return new UdpPeerDiscoveryTransport(new IPAddress(0xfeffff7f), port_, address, + new UdpPeerDiscoveryTransport.Options { MaxRetries = MaxRetries, MaxRetryDelayMs = 100 }); + } + + public UdpReorderedTransportBuilder(Random random, bool packetLoss) + { + random_ = random; + + relay_ = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + relay_.Bind(new IPEndPoint(new IPAddress(0xfeffff7f), port_)); + + // Disable exception on UDP connection reset (don't care). + uint IOC_IN = 0x80000000; + uint IOC_VENDOR = 0x18000000; + uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12; + relay_.IOControl((int)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null); + + if (packetLoss) + { + packetCounters = new Dictionary(); + } + + Task.Run(async () => + { + try + { + while (true) + { + byte[] buf_ = new byte[1024]; + var result = await relay_.ReceiveFromAsync(new ArraySegment(buf_), SocketFlags.None, new IPEndPoint(IPAddress.Any, 0)); + + if (packetCounters != null) + { + // Increase the probability of delivery of a packet with retries, up to 100% on the last retry. + // This simulates heavy packet loss while still guaranteeing that everything works. + // Note that this logic is very naive, but should be fine for small tests. + var packet = new Packet((IPEndPoint)result.RemoteEndPoint, new ArraySegment(buf_, 0, result.ReceivedBytes)); + if (!packetCounters.TryGetValue(packet, out int counter)) + { + counter = 0; + } + + if (counter == MaxRetries - 1) + { + // Last retry, always send and forget the packet. + packetCounters.Remove(packet); + } + else + { + packetCounters[packet] = counter + 1; + // Drop with decreasing probability. + if (random_.Next(0, MaxRetries) > counter) + { + continue; + } + } + } + + // The relay sends the packets to all peers with a random delay. + IPEndPoint[] curRecipients; + lock (recipients_) + { + curRecipients = recipients_.ToArray(); + } + foreach (var rec in curRecipients) + { + var delay = random_.Next(MaxDelayMs); + _ = Task.Delay(delay).ContinueWith(t => + { + try + { + relay_.SendToAsync(new ArraySegment(buf_, 0, result.ReceivedBytes), SocketFlags.None, rec); + } + catch (ObjectDisposedException) { } + catch (SocketException e) when (e.SocketErrorCode == SocketError.NotSocket) { } + }); + } + } + } + catch (ObjectDisposedException) { } + catch (SocketException e) when (e.SocketErrorCode == SocketError.NotSocket) { } + }); + } + + public void Dispose() + { + relay_.Dispose(); + } + } +} diff --git a/libs/Matchmaking/test/Matchmaking.PeerDiscovery.Test/PeerDiscoveryTest.cs b/libs/Matchmaking/test/Matchmaking.PeerDiscovery.Test/PeerDiscoveryTest.cs index 6118f84..d99031e 100644 --- a/libs/Matchmaking/test/Matchmaking.PeerDiscovery.Test/PeerDiscoveryTest.cs +++ b/libs/Matchmaking/test/Matchmaking.PeerDiscovery.Test/PeerDiscoveryTest.cs @@ -4,10 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; using System.Net; using System.Net.Sockets; -using System.Reflection.Metadata; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -17,13 +15,13 @@ namespace Microsoft.MixedReality.Sharing.Matchmaking.Test { public abstract class PeerDiscoveryTest { - protected Func discoveryAgentFactory_; - private readonly ITestOutputHelper output_; + internal ITransportBuilder transportBuilder_; + protected readonly ITestOutputHelper output_; protected readonly Random random_; - protected PeerDiscoveryTest(Func factory, ITestOutputHelper output) + internal PeerDiscoveryTest(ITransportBuilder transportBuilder, ITestOutputHelper output) { - discoveryAgentFactory_ = factory; + transportBuilder_ = transportBuilder; output_ = output; var seed = new Random().Next(); @@ -31,13 +29,17 @@ namespace Microsoft.MixedReality.Sharing.Matchmaking.Test random_ = new Random(seed); } - protected abstract bool SimulatesPacketLoss { get; } + protected virtual IDiscoveryAgent MakeAgent(int userIndex) + { + var transport = transportBuilder_.MakeTransport(userIndex); + return new PeerDiscoveryAgent(transport, new PeerDiscoveryAgent.Options { ResourceExpirySec = int.MaxValue }); + } [Fact] public void CreateResource() { using (var cts = new CancellationTokenSource(Utils.TestTimeoutMs)) - using (var svc1 = discoveryAgentFactory_(1)) + using (var svc1 = MakeAgent(1)) { var resource1 = svc1.PublishAsync("CreateResource", "http://resource1", null, cts.Token).Result; @@ -57,8 +59,8 @@ namespace Microsoft.MixedReality.Sharing.Matchmaking.Test public void FindResourcesLocalAndRemote() { using (var cts = new CancellationTokenSource(Utils.TestTimeoutMs)) - using (var svc1 = discoveryAgentFactory_(1)) - using (var svc2 = discoveryAgentFactory_(2)) + using (var svc1 = MakeAgent(1)) + using (var svc2 = MakeAgent(2)) { // Create some resources in the first one const string category = "FindResourcesLocalAndRemote"; @@ -109,8 +111,8 @@ namespace Microsoft.MixedReality.Sharing.Matchmaking.Test // start discovery, then start services afterwards using (var cts = new CancellationTokenSource(Utils.TestTimeoutMs)) - using (var svc1 = discoveryAgentFactory_(1)) - using (var svc2 = discoveryAgentFactory_(2)) + using (var svc1 = MakeAgent(1)) + using (var svc2 = MakeAgent(2)) { const string category = "FindResourcesFromAnnouncement"; @@ -137,7 +139,7 @@ namespace Microsoft.MixedReality.Sharing.Matchmaking.Test [Fact] public void AgentShutdownRemovesResources() { - if (SimulatesPacketLoss) + if (transportBuilder_.SimulatesPacketLoss) { // Shutdown message might not be retried and its loss will make this test fail. Skip it. return; @@ -148,7 +150,7 @@ namespace Microsoft.MixedReality.Sharing.Matchmaking.Test // start discovery, then start agents afterwards using (var cts = new CancellationTokenSource(Utils.TestTimeoutMs)) - using (var svc1 = discoveryAgentFactory_(1)) + using (var svc1 = MakeAgent(1)) using (var resources1 = svc1.Subscribe(category1)) using (var resources2 = svc1.Subscribe(category2)) { @@ -156,8 +158,8 @@ namespace Microsoft.MixedReality.Sharing.Matchmaking.Test // These are disposed manually, but keep in a using block so that they are disposed even // if the test throws. - using (var svc2 = discoveryAgentFactory_(2)) - using (var svc3 = discoveryAgentFactory_(3)) + using (var svc2 = MakeAgent(2)) + using (var svc3 = MakeAgent(3)) { // Create resources from svc2 and svc3 var resource2_1 = svc2.PublishAsync(category1, "conn1", null, cts.Token).Result; @@ -199,13 +201,13 @@ namespace Microsoft.MixedReality.Sharing.Matchmaking.Test public void CanEditResourceAttributes() { using (var cts = new CancellationTokenSource(Utils.TestTimeoutMs)) - using (var svc1 = discoveryAgentFactory_(1)) + using (var svc1 = MakeAgent(1)) { const string category = "CanEditResourceAttributes"; var resources1 = svc1.Subscribe(category); Assert.Empty(resources1.Resources); - using (var svc2 = discoveryAgentFactory_(2)) + using (var svc2 = MakeAgent(2)) { // Create resources from svc2 var origAttrs = new Dictionary { { "keyA", "valA" }, { "keyB", "valB" } }; @@ -269,11 +271,11 @@ namespace Microsoft.MixedReality.Sharing.Matchmaking.Test var delayedException = new Utils.DelayedException(); using (var cts = new CancellationTokenSource(Utils.TestTimeoutMs)) - using (var publishingAgent = discoveryAgentFactory_(1)) + using (var publishingAgent = MakeAgent(1)) { publishingAgent.PublishAsync(category1, "", null, cts.Token).Wait(); publishingAgent.PublishAsync(category2, "", null, cts.Token).Wait(); - var agent = discoveryAgentFactory_(2); + var agent = MakeAgent(2); { bool subIsDisposed = false; @@ -351,8 +353,8 @@ namespace Microsoft.MixedReality.Sharing.Matchmaking.Test using (var cts = new CancellationTokenSource(Utils.TestTimeoutMs)) { var delayedException = new Utils.DelayedException(); - using (var svc1 = discoveryAgentFactory_(1)) - using (var svc2 = discoveryAgentFactory_(2)) + using (var svc1 = MakeAgent(1)) + using (var svc2 = MakeAgent(2)) { for (int i = 0; i < 1000; ++i) { @@ -392,195 +394,26 @@ namespace Microsoft.MixedReality.Sharing.Matchmaking.Test public class PeerDiscoveryTestUdp : PeerDiscoveryTest { - static private IDiscoveryAgent MakeDiscoveryAgent(int userIndex) - { - var net = new UdpPeerDiscoveryTransport(new IPAddress(0xffffff7f), 45277, new IPAddress(0x0000007f + (userIndex << 24))); - return new PeerDiscoveryAgent(net, new PeerDiscoveryAgent.Options { ResourceExpirySec = int.MaxValue }); - } - - public PeerDiscoveryTestUdp(ITestOutputHelper output) : base(MakeDiscoveryAgent, output) { } - - protected override bool SimulatesPacketLoss => false; - } - - public class PeerDiscoveryTestUdpMulticast : PeerDiscoveryTest - { - static private IDiscoveryAgent MakeDiscoveryAgent(int userIndex) - { - var net = new UdpPeerDiscoveryTransport(new IPAddress(0x000000e0), 45278, new IPAddress(0x0000007f + (userIndex << 24))); - return new PeerDiscoveryAgent(net, new PeerDiscoveryAgent.Options { ResourceExpirySec = int.MaxValue }); - } - - public PeerDiscoveryTestUdpMulticast(ITestOutputHelper output) : base(MakeDiscoveryAgent, output) { } - protected override bool SimulatesPacketLoss => false; + public PeerDiscoveryTestUdp(ITestOutputHelper output) : base(new UdpTransportBuilder(), output) { } } public class PeerDiscoveryTestMemory : PeerDiscoveryTest { - static private IDiscoveryAgent MakeDiscoveryAgent(int userIndex) - { - var net = new MemoryPeerDiscoveryTransport(userIndex); - return new PeerDiscoveryAgent(net, new PeerDiscoveryAgent.Options { ResourceExpirySec = int.MaxValue }); - } - - public PeerDiscoveryTestMemory(ITestOutputHelper output) : base(MakeDiscoveryAgent, output) { } - protected override bool SimulatesPacketLoss => false; + public PeerDiscoveryTestMemory(ITestOutputHelper output) : base(new MemoryTransportBuilder(), output) { } } // Uses relay socket to reorder packets delivery. - public abstract class PeerDiscoveryTestReordered : PeerDiscoveryTest, IDisposable + public class PeerDiscoveryTestUdpUnreliable : PeerDiscoveryTest, IDisposable { - private const int MaxDelayMs = 25; - private const int MaxRetries = 3; - - private readonly Socket relay_; - private readonly ushort port_; - private readonly List recipients_ = new List(); - - // Wraps a packet for use in a map. - private class Packet + public PeerDiscoveryTestUdpUnreliable(ITestOutputHelper output) + : base(null, output) { - public IPEndPoint EndPoint; - public byte[] Contents; - - public Packet(IPEndPoint endPoint, ArraySegment contents) - { - EndPoint = endPoint; - Contents = new byte[contents.Count]; - for (int i = 0; i < contents.Count; ++i) - { - Contents[i] = contents[i]; - } - } - - public override bool Equals(object other) - { - if (other is Packet rhs) - { - return EndPoint.Equals(rhs.EndPoint) && Contents.SequenceEqual(rhs.Contents); - } - return false; - } - - public override int GetHashCode() - { - // Not a great hash but it shouldn't matter in this case. - var result = 0; - foreach (byte b in Contents) - { - result = (result * 31) ^ b; - } - return EndPoint.GetHashCode() ^ result; - } - } - - // Keeps track of each packet that goes through the relay and counts its repetitions. - // See receive loop for usage. - private readonly Dictionary packetCounters; - - protected override bool SimulatesPacketLoss => (packetCounters != null); - - private IDiscoveryAgent MakeDiscoveryAgent(int userIndex) - { - // Peers all send packets to the relay. - var address = new IPAddress(0x0000007f + (userIndex << 24)); - var net = new UdpPeerDiscoveryTransport(new IPAddress(0xfeffff7f), port_, address, - new UdpPeerDiscoveryTransport.Options { MaxRetries = MaxRetries, MaxRetryDelayMs = 100 }); - lock (recipients_) - { - var endpoint = new IPEndPoint(address, port_); - - // Remove first so the same agent can be re-created multiple times - recipients_.Remove(endpoint); - recipients_.Add(endpoint); - } - return new PeerDiscoveryAgent(net, new PeerDiscoveryAgent.Options { ResourceExpirySec = int.MaxValue }); - } - - public PeerDiscoveryTestReordered(ITestOutputHelper output, bool packetLoss, ushort port) : base(null, output) - { - discoveryAgentFactory_ = MakeDiscoveryAgent; - port_ = port; - - relay_ = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - relay_.Bind(new IPEndPoint(new IPAddress(0xfeffff7f), port_)); - - // Disable exception on UDP connection reset (don't care). - uint IOC_IN = 0x80000000; - uint IOC_VENDOR = 0x18000000; - uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12; - relay_.IOControl((int)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null); - - if (packetLoss) - { - packetCounters = new Dictionary(); - } - - Task.Run(async () => - { - try - { - while (true) - { - byte[] buf_ = new byte[1024]; - var result = await relay_.ReceiveFromAsync(new ArraySegment(buf_), SocketFlags.None, new IPEndPoint(IPAddress.Any, 0)); - - if (packetCounters != null) - { - // Increase the probability of delivery of a packet with retries, up to 100% on the last retry. - // This simulates heavy packet loss while still guaranteeing that everything works. - // Note that this logic is very naive, but should be fine for small tests. - var packet = new Packet((IPEndPoint)result.RemoteEndPoint, new ArraySegment(buf_, 0, result.ReceivedBytes)); - if (!packetCounters.TryGetValue(packet, out int counter)) - { - counter = 0; - } - - if (counter == MaxRetries - 1) - { - // Last retry, always send and forget the packet. - packetCounters.Remove(packet); - } - else - { - packetCounters[packet] = counter + 1; - // Drop with decreasing probability. - if (random_.Next(0, MaxRetries) > counter) - { - continue; - } - } - } - - // The relay sends the packets to all peers with a random delay. - IPEndPoint[] curRecipients; - lock (recipients_) - { - curRecipients = recipients_.ToArray(); - } - foreach (var rec in curRecipients) - { - var delay = random_.Next(MaxDelayMs); - _ = Task.Delay(delay).ContinueWith(t => - { - try - { - relay_.SendToAsync(new ArraySegment(buf_, 0, result.ReceivedBytes), SocketFlags.None, rec); - } - catch (ObjectDisposedException) { } - catch (SocketException e) when (e.SocketErrorCode == SocketError.NotSocket) { } - }); - } - } - } - catch (ObjectDisposedException) { } - catch (SocketException e) when (e.SocketErrorCode == SocketError.NotSocket) { } - }); + transportBuilder_ = new UdpReorderedTransportBuilder(random_, packetLoss: true); } public void Dispose() { - relay_.Dispose(); + ((IDisposable)transportBuilder_).Dispose(); } [Fact] @@ -588,9 +421,9 @@ namespace Microsoft.MixedReality.Sharing.Matchmaking.Test { const string category = "AttributeEditsAreInOrder"; using (var cts = new CancellationTokenSource(Utils.TestTimeoutMs)) - using (var svc1 = discoveryAgentFactory_(1)) + using (var svc1 = MakeAgent(1)) using (var discovery = svc1.Subscribe(category)) - using (var svc2 = discoveryAgentFactory_(2)) + using (var svc2 = MakeAgent(2)) { // Create resource from svc2 var origAttrs = new Dictionary { { "value", "0" } }; @@ -615,7 +448,7 @@ namespace Microsoft.MixedReality.Sharing.Matchmaking.Test } // Give some time to the messages to reach the peers. - Task.Delay(MaxDelayMs).Wait(); + Task.Delay(UdpReorderedTransportBuilder.MaxDelayMs).Wait(); // Edits should show up in svc1 in order { @@ -650,19 +483,34 @@ namespace Microsoft.MixedReality.Sharing.Matchmaking.Test } } - public class PeerDiscoveryTestReorderedReliable : PeerDiscoveryTestReordered + // This test needs reordering but does not work with packet loss, so we use a separate class. + public class PeerDiscoveryTestReorderedReliable { - public PeerDiscoveryTestReorderedReliable(ITestOutputHelper output) : base(output, packetLoss: false, 45279) { } + private readonly ITransportBuilder transportBuilder_; + + public PeerDiscoveryTestReorderedReliable(ITestOutputHelper output) + { + var seed = new Random().Next(); + output.WriteLine($"Seed for lossy network: {seed}"); + var random = new Random(seed); + transportBuilder_ = new UdpReorderedTransportBuilder(random, packetLoss: false); + } + + protected virtual IDiscoveryAgent MakeAgent(int userIndex) + { + var transport = transportBuilder_.MakeTransport(userIndex); + return new PeerDiscoveryAgent(transport, new PeerDiscoveryAgent.Options { ResourceExpirySec = int.MaxValue }); + } [Fact] public void NoAnnouncementsAfterDispose() { const string category = "NoAnnouncementsAfterDispose"; using (var cts = new CancellationTokenSource(Utils.TestTimeoutMs)) - using (var svc1 = discoveryAgentFactory_(1)) + using (var svc1 = MakeAgent(1)) using (var discovery = svc1.Subscribe(category)) { - using (var svc2 = discoveryAgentFactory_(2)) + using (var svc2 = MakeAgent(2)) { // Create a lot of resources. var tasks = new Task[100]; @@ -685,8 +533,4 @@ namespace Microsoft.MixedReality.Sharing.Matchmaking.Test } } } - public class PeerDiscoveryTestReorderedUnreliable : PeerDiscoveryTestReordered - { - public PeerDiscoveryTestReorderedUnreliable(ITestOutputHelper output) : base(output, packetLoss: true, 45280) { } - } } \ No newline at end of file diff --git a/libs/Matchmaking/test/Matchmaking.PeerDiscovery.Test/TransportTest.cs b/libs/Matchmaking/test/Matchmaking.PeerDiscovery.Test/TransportTest.cs new file mode 100644 index 0000000..d673f96 --- /dev/null +++ b/libs/Matchmaking/test/Matchmaking.PeerDiscovery.Test/TransportTest.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.MixedReality.Sharing.Matchmaking.Test +{ + public class TransportTest + { + private readonly Random random_; + + public TransportTest(ITestOutputHelper output) + { + var seed = new Random().Next(); + output.WriteLine($"Seed for lossy network: {seed}"); + random_ = new Random(seed); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TestMemory(bool useMultiThread) { SendReceive(new MemoryTransportBuilder(), useMultiThread); } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TestUdp(bool useMultiThread) { SendReceive(new UdpTransportBuilder(), useMultiThread); } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TestUdpMulticast(bool useMultiThread) { SendReceive(new UdpMulticastTransportBuilder(), useMultiThread); } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TestUdpReordered(bool useMultiThread) + { + SendReceive(new UdpReorderedTransportBuilder(random_, false), useMultiThread); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TestUdpReorderedUnreliable(bool useMultiThread) + { + SendReceive(new UdpReorderedTransportBuilder(random_, true), useMultiThread); + } + + private void SendReceive(ITransportBuilder transportBuilder, bool useMultiThread) + { + using(var cts = new CancellationTokenSource(Utils.TestTimeoutMs)) + { + const int lastId = 50; + const int guidsPerTransport = 3; + + // Make a few transports in the same broadcast domain. + var transports = new IPeerDiscoveryTransport[5]; + + // Associate a few stream IDs to each transport. + int streamsNum = guidsPerTransport * transports.Length; + var streams = new Guid[streamsNum]; + + // Keep track of the messages received by each transport. + var lastIdFromGuid = new Dictionary[transports.Length]; + var lastMessageReceived = new Dictionary[transports.Length]; + + + for (int transportIdx = 0; transportIdx < transports.Length; ++transportIdx) + { + // Initialize transport and associated data. + lastMessageReceived[transportIdx] = new Dictionary(); + lastIdFromGuid[transportIdx] = new Dictionary(); + for (int guidIdx = 0; guidIdx < guidsPerTransport; ++guidIdx) + { + var streamId = Guid.NewGuid(); + streams[transportIdx * guidsPerTransport + guidIdx] = streamId; + lastMessageReceived[transportIdx][streamId] = new ManualResetEvent(false); + } + transports[transportIdx] = transportBuilder.MakeTransport(transportIdx + 1); + + // Handle received messages. + // Note: must copy into local variable or the lambda will capture the wrong value. + int captureTransportIdx = transportIdx; + transports[transportIdx].Message += + (IPeerDiscoveryTransport _, IPeerDiscoveryMessage message) => + { + // Check that messages are received in send order. + lastIdFromGuid[captureTransportIdx].TryGetValue(message.StreamId, out int lastReceivedId); + int currentId = BitConverter.ToInt32(message.Contents); + Assert.True(currentId > lastReceivedId); + + // Notify that the last message hasn't been dropped. + if (currentId == lastId) + { + lastMessageReceived[captureTransportIdx][message.StreamId].Set(); + } + }; + transports[transportIdx].Start(); + } + + try + { + // Send a sequence of messages from every transport for every stream. + var sendTasks = new List(); + for (int transportIdx = 0; transportIdx < transports.Length; ++transportIdx) + { + for (int guidIdx = 0; guidIdx < guidsPerTransport; ++guidIdx) + { + var guid = streams[transportIdx * guidsPerTransport + guidIdx]; + var transport = transports[transportIdx]; + Action sendMessages = () => + { + for (int msgIdx = 1; msgIdx <= lastId; ++msgIdx) + { + byte[] bytes = BitConverter.GetBytes(msgIdx); + transport.Broadcast(guid, bytes); + } + }; + if (useMultiThread) + { + sendTasks.Add(Task.Run(sendMessages)); + } + else + { + sendMessages(); + } + } + } + + // Wait until all messages have been sent. + Task.WaitAll(sendTasks.ToArray(), cts.Token); + + // The last message has been received for every stream by every transport. + var allHandles = new List(streamsNum); + foreach (var transportEvents in lastMessageReceived) + { + foreach(var ev in transportEvents.Values) + { + allHandles.Add(ev); + } + } + // Workaround to WaitHandle.WaitAll not taking a CancellationToken. + Task.Run(() => WaitHandle.WaitAll(allHandles.ToArray())).Wait(cts.Token); + } + finally + { + Exception anyException = null; + foreach (var transport in transports) + { + try + { + transport.Stop(); + } + catch (Exception e) + { + anyException = e; + } + } + if (anyException != null) + { + throw anyException; + } + } + } + } + } +} diff --git a/libs/Matchmaking/test/Matchmaking.PeerDiscovery.Test/Utils.cs b/libs/Matchmaking/test/Matchmaking.PeerDiscovery.Test/Utils.cs index 0387741..3c8571c 100644 --- a/libs/Matchmaking/test/Matchmaking.PeerDiscovery.Test/Utils.cs +++ b/libs/Matchmaking/test/Matchmaking.PeerDiscovery.Test/Utils.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.MixedReality.Sharing.Matchmaking; using System; using System.Collections.Generic; using System.Diagnostics; @@ -45,6 +44,13 @@ namespace Microsoft.MixedReality.Sharing.Matchmaking.Test } } + public static class NewPortNumber + { + private static volatile ushort current_ = 40000; + + public static ushort Calculate() { return ++current_; } + } + // Run a query and wait for the predicate to be satisfied. // Return the list of resources which satisfied the predicate or null if canceled before the predicate was satisfied. public static IEnumerable QueryAndWaitForResourcesPredicate(