Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added basic end2end tests #183

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
4 changes: 4 additions & 0 deletions NearShare.sln
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,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>();
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
13 changes: 13 additions & 0 deletions lib/ShortDev.Microsoft.ConnectedDevices/TestUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Net;

namespace ShortDev.Microsoft.ConnectedDevices;

internal sealed class TestUtils
{
public static IPAddress ListenAddress { get; private set; } = IPAddress.Any;

internal static void ListenLocalOnly()
{
ListenAddress = IPAddress.Loopback;
}
}
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(TestUtils.ListenAddress, 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)

#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(TestUtils.ListenAddress, 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
Loading