177 lines
6.3 KiB
C#
177 lines
6.3 KiB
C#
using System.IO;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using NUnit.Framework;
|
|
using MCPForUnity.Editor.Helpers;
|
|
|
|
namespace MCPForUnityTests.Editor.Services
|
|
{
|
|
[TestFixture]
|
|
public class PortManagerTests
|
|
{
|
|
private string _savedPortFileContent;
|
|
private string _savedLegacyFileContent;
|
|
private string _portFilePath;
|
|
private string _legacyFilePath;
|
|
|
|
[SetUp]
|
|
public void SetUp()
|
|
{
|
|
// Snapshot the on-disk port config so DiscoverNewPort tests don't
|
|
// permanently alter the running bridge's persisted port.
|
|
string dir = Path.Combine(
|
|
System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile),
|
|
".unity-mcp");
|
|
_legacyFilePath = Path.Combine(dir, "unity-mcp-port.json");
|
|
|
|
// The hashed file uses a private helper; approximate the same hash.
|
|
// We snapshot every json file in the directory to be safe.
|
|
_portFilePath = null;
|
|
_savedPortFileContent = null;
|
|
_savedLegacyFileContent = null;
|
|
|
|
if (File.Exists(_legacyFilePath))
|
|
_savedLegacyFileContent = File.ReadAllText(_legacyFilePath);
|
|
|
|
// Find the hashed port file for this project
|
|
if (Directory.Exists(dir))
|
|
{
|
|
foreach (var f in Directory.GetFiles(dir, "unity-mcp-port-*.json"))
|
|
{
|
|
_portFilePath = f;
|
|
_savedPortFileContent = File.ReadAllText(f);
|
|
break; // one project at a time
|
|
}
|
|
}
|
|
}
|
|
|
|
[TearDown]
|
|
public void TearDown()
|
|
{
|
|
// Restore the original port files
|
|
if (_savedLegacyFileContent != null && _legacyFilePath != null)
|
|
File.WriteAllText(_legacyFilePath, _savedLegacyFileContent);
|
|
|
|
if (_savedPortFileContent != null && _portFilePath != null)
|
|
File.WriteAllText(_portFilePath, _savedPortFileContent);
|
|
}
|
|
|
|
[Test]
|
|
public void IsPortAvailable_ReturnsFalse_WhenPortIsOccupied()
|
|
{
|
|
var listener = new TcpListener(IPAddress.Loopback, 0);
|
|
listener.Start();
|
|
int port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
|
|
|
try
|
|
{
|
|
Assert.IsFalse(PortManager.IsPortAvailable(port),
|
|
"IsPortAvailable should return false for a port that is already bound");
|
|
}
|
|
finally
|
|
{
|
|
listener.Stop();
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void IsPortAvailable_ReturnsTrue_WhenPortIsFree()
|
|
{
|
|
var listener = new TcpListener(IPAddress.Loopback, 0);
|
|
listener.Start();
|
|
int port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
|
listener.Stop();
|
|
|
|
Assert.IsTrue(PortManager.IsPortAvailable(port),
|
|
"IsPortAvailable should return true for a port that is not bound");
|
|
}
|
|
|
|
#if UNITY_EDITOR_OSX
|
|
[Test]
|
|
public void IsPortAvailable_ReturnsFalse_WhenPortHeldWithReuseAddr()
|
|
{
|
|
// Simulate what AssetImportWorkers do: bind with SO_REUSEADDR.
|
|
// IsPortAvailable must still detect this as occupied.
|
|
var holder = new TcpListener(IPAddress.Loopback, 0);
|
|
holder.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
|
holder.Start();
|
|
int port = ((IPEndPoint)holder.LocalEndpoint).Port;
|
|
|
|
try
|
|
{
|
|
Assert.IsFalse(PortManager.IsPortAvailable(port),
|
|
"IsPortAvailable should detect ports held with SO_REUSEADDR on macOS");
|
|
}
|
|
finally
|
|
{
|
|
holder.Stop();
|
|
}
|
|
}
|
|
#endif
|
|
|
|
[Test]
|
|
public void ShouldAbandonBusyPort_KeepsSamePort_WithinReleaseWindow()
|
|
{
|
|
// A port busy for less than the fallback window is treated as our own
|
|
// not-yet-released listener after a domain reload — keep retrying the same
|
|
// port instead of silently switching and stranding the client (#1173).
|
|
Assert.IsFalse(PortManager.ShouldAbandonBusyPort(0.0));
|
|
Assert.IsFalse(PortManager.ShouldAbandonBusyPort(
|
|
PortManager.BusyPortFallbackWindowSeconds - 0.5));
|
|
}
|
|
|
|
[Test]
|
|
public void ShouldAbandonBusyPort_FallsBack_AfterReleaseWindow()
|
|
{
|
|
// A port that stays busy past the window is a foreign occupant — only then
|
|
// does the bridge discover and switch to a new port.
|
|
Assert.IsTrue(PortManager.ShouldAbandonBusyPort(
|
|
PortManager.BusyPortFallbackWindowSeconds));
|
|
Assert.IsTrue(PortManager.ShouldAbandonBusyPort(
|
|
PortManager.BusyPortFallbackWindowSeconds + 5.0));
|
|
}
|
|
|
|
[Test]
|
|
public void DiscoverNewPort_ReturnsAvailablePort()
|
|
{
|
|
int port = PortManager.DiscoverNewPort();
|
|
Assert.Greater(port, 0, "DiscoverNewPort should return a positive port number");
|
|
Assert.IsTrue(PortManager.IsPortAvailable(port),
|
|
"The port returned by DiscoverNewPort should be available");
|
|
}
|
|
|
|
[Test]
|
|
public void DiscoverNewPort_SkipsOccupiedDefaultPort()
|
|
{
|
|
// Hold the default port (6400) so DiscoverNewPort must find an alternative
|
|
TcpListener holder = null;
|
|
try
|
|
{
|
|
holder = new TcpListener(IPAddress.Loopback, 6400);
|
|
#if UNITY_EDITOR_OSX
|
|
try { holder.Server.ExclusiveAddressUse = true; } catch { }
|
|
#endif
|
|
holder.Start();
|
|
}
|
|
catch (SocketException)
|
|
{
|
|
// Port 6400 already occupied (e.g., by the running bridge) — that's fine,
|
|
// the test still validates that DiscoverNewPort picks a different port.
|
|
holder = null;
|
|
}
|
|
|
|
try
|
|
{
|
|
int port = PortManager.DiscoverNewPort();
|
|
Assert.AreNotEqual(6400, port,
|
|
"DiscoverNewPort should not return the default port when it is occupied");
|
|
Assert.Greater(port, 0);
|
|
}
|
|
finally
|
|
{
|
|
holder?.Stop();
|
|
}
|
|
}
|
|
}
|
|
}
|