Skip to content

Commit

Permalink
Added basic end2end tests (#183)
Browse files Browse the repository at this point in the history
  • Loading branch information
ShortDevelopment authored Jan 4, 2025
1 parent 3390897 commit d22c471
Show file tree
Hide file tree
Showing 12 changed files with 416 additions and 32 deletions.
4 changes: 4 additions & 0 deletions NearShare.sln
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CdpSvcUtil", "utils\CdpSvcUtil\CdpSvcUtil.vcxproj", "{CFABE26A-FBFF-4CF9-8C94-B603B317A223}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F0E5B489-0FD2-4A7E-B660-C28450479583}"
ProjectSection(SolutionItems) = preProject
dockerfile.test = dockerfile.test
testenvironments.json = testenvironments.json
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ShortDev.Microsoft.ConnectedDevices.Test", "tests\ShortDev.Microsoft.ConnectedDevices.Test\ShortDev.Microsoft.ConnectedDevices.Test.csproj", "{B0DE3385-5FD7-4D05-8296-6E298A3F1BA2}"
EndProject
Expand Down
4 changes: 4 additions & 0 deletions dockerfile.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
FROM mcr.microsoft.com/dotnet/sdk:9.0

RUN wget https://aka.ms/getvsdbgsh && \
sh getvsdbgsh -v latest -l /vsdbg
10 changes: 5 additions & 5 deletions lib/ShortDev.Microsoft.ConnectedDevices/EndpointInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ public IPEndPoint ToIPEndPoint()
}

public static EndpointInfo FromTcp(IPEndPoint endpoint)
=> FromTcp(endpoint.Address);
=> FromTcp(endpoint.Address, endpoint.Port);

public static EndpointInfo FromTcp(IPAddress address)
=> FromTcp(address.ToString());
public static EndpointInfo FromTcp(IPAddress address, int port = Constants.TcpPort)
=> FromTcp(address.ToString(), port);

public static EndpointInfo FromTcp(string address)
=> new(CdpTransportType.Tcp, address, Constants.TcpPort.ToString());
public static EndpointInfo FromTcp(string address, int port = Constants.TcpPort)
=> new(CdpTransportType.Tcp, address, port.ToString());

public static EndpointInfo FromRfcommDevice(PhysicalAddress macAddress)
=> new(CdpTransportType.Rfcomm, macAddress.ToStringFormatted(), Constants.RfcommServiceId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,9 @@ void HandleUpgradeRequest(CdpSocket socket, ref EndianReader reader)
Type = MessageType.Connect
};

var localIp = _session.Platform.TryGetTransport<NetworkTransport>()?.Handler.TryGetLocalIp();
if (localIp == null)
var networkTransport = _session.Platform.TryGetTransport<NetworkTransport>();

Check warning on line 98 in lib/ShortDev.Microsoft.ConnectedDevices/Session/Upgrade/HostUpgradeHandler.cs

View workflow job for this annotation

GitHub Actions / ShortDev.Microsoft.ConnectedDevices.Test

'ConnectedDevicesPlatform.TryGetTransport<T>()' is obsolete: 'Use overload instead'

Check warning on line 98 in lib/ShortDev.Microsoft.ConnectedDevices/Session/Upgrade/HostUpgradeHandler.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

'ConnectedDevicesPlatform.TryGetTransport<T>()' is obsolete: 'Use overload instead'
var localIp = networkTransport?.Handler.TryGetLocalIp();
if (networkTransport == null || localIp == null)
{
EndianWriter writer = new(Endianness.BigEndian);
new ConnectionHeader()
Expand Down Expand Up @@ -126,7 +127,7 @@ void HandleUpgradeRequest(CdpSocket socket, ref EndianReader reader)
{
Endpoints =
[
EndpointInfo.FromTcp(localIp)
EndpointInfo.FromTcp(localIp, networkTransport.TcpPort)
],
MetaData =
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,32 @@

namespace ShortDev.Microsoft.ConnectedDevices.Transports.Network;

public sealed class NetworkTransport(INetworkHandler handler) : ICdpTransport, ICdpDiscoverableTransport
public sealed class NetworkTransport(
INetworkHandler handler,
int tcpPort = Constants.TcpPort, int udpPort = Constants.UdpPort
) : ICdpTransport, ICdpDiscoverableTransport
{
readonly TcpListener _listener = new(IPAddress.Any, Constants.TcpPort);
public int TcpPort { get; } = tcpPort;
public int UdpPort { get; } = udpPort;

TcpListener? _listener;
public INetworkHandler Handler { get; } = handler;

public CdpTransportType TransportType { get; } = CdpTransportType.Tcp;
public EndpointInfo GetEndpoint()
=> new(TransportType, Handler.GetLocalIp().ToString(), Constants.TcpPort.ToString(CultureInfo.InvariantCulture));
=> new(TransportType, Handler.GetLocalIp().ToString(), TcpPort.ToString(CultureInfo.InvariantCulture));

public event DeviceConnectedEventHandler? DeviceConnected;
public async Task Listen(CancellationToken cancellationToken)
{
_listener.Start();
var listener = _listener ??= new(IPAddress.Any, TcpPort);
listener.Start();

try
{
while (!cancellationToken.IsCancellationRequested)
{
var client = await _listener.AcceptTcpClientAsync(cancellationToken).ConfigureAwait(false);
var client = await listener.AcceptTcpClientAsync(cancellationToken).ConfigureAwait(false);

if (client.Client.RemoteEndPoint is not IPEndPoint endPoint)
return;
Expand All @@ -39,7 +46,7 @@ public async Task Listen(CancellationToken cancellationToken)
Endpoint = new EndpointInfo(
TransportType,
endPoint.Address.ToString(),
Constants.TcpPort.ToString(CultureInfo.InvariantCulture)
TcpPort.ToString(CultureInfo.InvariantCulture)
)
});
}
Expand All @@ -65,10 +72,26 @@ public async Task<CdpSocket> ConnectAsync(EndpointInfo endpoint, CancellationTok

#region Discovery (Udp)

readonly UdpClient _udpclient = new(Constants.UdpPort)
readonly UdpClient _udpclient = CreateUdpClient(udpPort);

static UdpClient CreateUdpClient(int port)
{
EnableBroadcast = true
};
UdpClient client = new()
{
EnableBroadcast = true
};

if (OperatingSystem.IsWindows())
{
const int SIO_UDP_CONNRESET = -1744830452;
client.Client.IOControl(SIO_UDP_CONNRESET, [0, 0, 0, 0], null);
}

client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
client.Client.Bind(new IPEndPoint(IPAddress.Any, port));

return client;
}

public async Task Advertise(LocalDeviceInfo deviceInfo, CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -194,7 +217,7 @@ void SendPresenceRequest()
Type = DiscoveryType.PresenceRequest
}.Write(payloadWriter);

new UdpFragmentSender(_udpclient, new IPEndPoint(IPAddress.Broadcast, Constants.UdpPort))
new UdpFragmentSender(_udpclient, new IPEndPoint(IPAddress.Broadcast, UdpPort))
.SendMessage(header, payloadWriter.Buffer.AsSpan());
}

Expand All @@ -212,7 +235,7 @@ void SendPresenceResponse(IPAddress device, PresenceResponse response)
}.Write(payloadWriter);
response.Write(payloadWriter);

new UdpFragmentSender(_udpclient, new IPEndPoint(device, Constants.UdpPort))
new UdpFragmentSender(_udpclient, new IPEndPoint(device, UdpPort))
.SendMessage(header, payloadWriter.Buffer.AsSpan());
}

Expand All @@ -228,7 +251,7 @@ public void Dispose()
DeviceDiscovered = null;
DiscoveryMessageReceived = null;

_listener.Dispose();
_listener?.Dispose();
_udpclient.Dispose();
}
}
10 changes: 10 additions & 0 deletions testenvironments.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"version": "1",
"environments": [
{
"name": ".NET 9 Linux",
"type": "docker",
"dockerFile": "dockerfile.test"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using ShortDev.Microsoft.ConnectedDevices.Transports;
using ShortDev.Microsoft.ConnectedDevices.Transports.Bluetooth;
using System.IO.Pipes;
using System.Net.NetworkInformation;

namespace ShortDev.Microsoft.ConnectedDevices.Test.E2E;

internal sealed class BluetoothHandler(DeviceContainer container, DeviceContainer.Device device) : IBluetoothHandler
{
public PhysicalAddress MacAddress => PhysicalAddress.Parse(device.Address);

public Task<CdpSocket> ConnectRfcommAsync(EndpointInfo endpoint, RfcommOptions options, CancellationToken cancellationToken = default)
{
var device = container.FindDevice(endpoint.Address)
?? throw new KeyNotFoundException("Could not find device");

return Task.FromResult(
device.ConnectFrom(new(CdpTransportType.Rfcomm, device.Address, options.ServiceId ?? ""))
);
}

public async Task ListenRfcommAsync(RfcommOptions options, CancellationToken cancellationToken = default)
{
device.ConnectionRequest += OnNewConnection;

await cancellationToken.AwaitCancellation();

device.ConnectionRequest -= OnNewConnection;

void OnNewConnection(EndpointInfo client, ref (Stream Input, Stream Output)? clientStream)
{
AnonymousPipeServerStream serverInputStream = new(PipeDirection.In);
AnonymousPipeServerStream serverOutputStream = new(PipeDirection.Out);

// Accept connection
clientStream = (
new AnonymousPipeClientStream(PipeDirection.In, serverOutputStream.GetClientHandleAsString()),
new AnonymousPipeClientStream(PipeDirection.Out, serverInputStream.GetClientHandleAsString())
);

options.SocketConnected?.Invoke(new CdpSocket()
{
InputStream = serverInputStream,
OutputStream = serverOutputStream,
Endpoint = client,
Close = () =>
{
serverInputStream.Dispose();
serverOutputStream.Dispose();
}
});
}
}

public async Task AdvertiseBLeBeaconAsync(AdvertiseOptions options, CancellationToken cancellationToken = default)
{
var data = options.BeaconData.ToArray();
container.Advertise(device, (uint)options.ManufacturerId, data);

await cancellationToken.AwaitCancellation();

container.TryRemove(device);
}

public async Task ScanBLeAsync(ScanOptions scanOptions, CancellationToken cancellationToken = default)
{
container.FoundDevice += OnNewDevice;

await cancellationToken.AwaitCancellation();

container.FoundDevice -= OnNewDevice;

void OnNewDevice(DeviceContainer.Device device, DeviceContainer.Adverstisement ad)
{
if (ad.Manufacturer != Constants.BLeBeaconManufacturerId)
return;

if (!BLeBeacon.TryParse(ad.Data.ToArray(), out var beaconData))
return;

scanOptions.OnDeviceDiscovered?.Invoke(beaconData);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using ShortDev.Microsoft.ConnectedDevices.Transports;
using System.Collections.Concurrent;

namespace ShortDev.Microsoft.ConnectedDevices.Test.E2E;

internal sealed class DeviceContainer
{
readonly ConcurrentDictionary<Device, List<Adverstisement>> _registry = [];
sealed record Entry(Device Device, List<Adverstisement> Adverstisements);

public Device? FindDevice(string address)
=> _registry.FirstOrDefault(x => x.Key.Address == address).Key;

public void Advertise(Device device, uint manufacturer, ReadOnlyMemory<byte> data)
{
var list = _registry.GetOrAdd(device, static key => []);
lock (list)
{
list.Add(new(manufacturer, data));
}
FoundDevice?.Invoke(device, new(manufacturer, data));
}

public bool TryRemove(Device device)
=> _registry.Remove(device, out _);

public event Action<Device, Adverstisement>? FoundDevice;

public sealed record Adverstisement(uint Manufacturer, ReadOnlyMemory<byte> Data);
public sealed record Device(CdpTransportType TransportType, string Address)
{
public CdpSocket ConnectFrom(EndpointInfo client)
{
(Stream Input, Stream Output)? stream = null;
ConnectionRequest?.Invoke(client, ref stream);

if (stream is null)
throw new InvalidOperationException("Server did not accept");

return new CdpSocket()
{
InputStream = stream.Value.Input,
OutputStream = stream.Value.Output,
Endpoint = new(TransportType, Address, "Some Service Id"),
Close = () =>
{
stream.Value.Output.Dispose();
stream.Value.Input.Dispose();
}
};
}

public event ConnectionRequestHandler? ConnectionRequest;

public delegate void ConnectionRequestHandler(EndpointInfo client, ref (Stream Input, Stream Output)? stream);
}
}
Loading

0 comments on commit d22c471

Please sign in to comment.