chore: import upstream snapshot with attribution
This commit is contained in:
+494
@@ -0,0 +1,494 @@
|
||||
using System.IO;
|
||||
using NUnit.Framework;
|
||||
using MCPForUnity.Editor.Services.Server;
|
||||
using MCPForUnity.Editor.Constants;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Services.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Unit tests for PidFileManager component.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class PidFileManagerTests
|
||||
{
|
||||
private PidFileManager _manager;
|
||||
private string _testPidFilePath;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_manager = new PidFileManager();
|
||||
// Clear any test state
|
||||
ClearTestEditorPrefs();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
// Clean up test files
|
||||
if (!string.IsNullOrEmpty(_testPidFilePath) && File.Exists(_testPidFilePath))
|
||||
{
|
||||
try { File.Delete(_testPidFilePath); } catch { }
|
||||
}
|
||||
// Clear test state
|
||||
ClearTestEditorPrefs();
|
||||
}
|
||||
|
||||
private void ClearTestEditorPrefs()
|
||||
{
|
||||
try { EditorPrefs.DeleteKey(EditorPrefKeys.LastLocalHttpServerPid); } catch { }
|
||||
try { EditorPrefs.DeleteKey(EditorPrefKeys.LastLocalHttpServerPort); } catch { }
|
||||
try { EditorPrefs.DeleteKey(EditorPrefKeys.LastLocalHttpServerStartedUtc); } catch { }
|
||||
try { EditorPrefs.DeleteKey(EditorPrefKeys.LastLocalHttpServerPidArgsHash); } catch { }
|
||||
try { EditorPrefs.DeleteKey(EditorPrefKeys.LastLocalHttpServerPidFilePath); } catch { }
|
||||
try { EditorPrefs.DeleteKey(EditorPrefKeys.LastLocalHttpServerInstanceToken); } catch { }
|
||||
}
|
||||
|
||||
#region GetPidFilePath Tests
|
||||
|
||||
[Test]
|
||||
public void GetPidFilePath_ValidPort_ReturnsCorrectPath()
|
||||
{
|
||||
// Act
|
||||
string path = _manager.GetPidFilePath(8080);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(path);
|
||||
Assert.That(path, Does.Contain("mcp_http_8080.pid"));
|
||||
Assert.That(path, Does.Contain("MCPForUnity"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetPidFilePath_DifferentPorts_ReturnsDifferentPaths()
|
||||
{
|
||||
// Act
|
||||
string path1 = _manager.GetPidFilePath(8080);
|
||||
string path2 = _manager.GetPidFilePath(9090);
|
||||
|
||||
// Assert
|
||||
Assert.AreNotEqual(path1, path2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetPidDirectory_ReturnsValidPath()
|
||||
{
|
||||
// Act
|
||||
string dir = _manager.GetPidDirectory();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(dir);
|
||||
Assert.That(dir, Does.Contain("MCPForUnity"));
|
||||
Assert.That(dir, Does.Contain("RunState"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TryReadPid Tests
|
||||
|
||||
[Test]
|
||||
public void TryReadPid_ValidFile_ReturnsTrueWithPid()
|
||||
{
|
||||
// Arrange
|
||||
_testPidFilePath = _manager.GetPidFilePath(59998);
|
||||
File.WriteAllText(_testPidFilePath, "12345");
|
||||
|
||||
// Act
|
||||
bool result = _manager.TryReadPid(_testPidFilePath, out int pid);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result);
|
||||
Assert.AreEqual(12345, pid);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryReadPid_FileWithWhitespace_ParsesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
_testPidFilePath = _manager.GetPidFilePath(59997);
|
||||
File.WriteAllText(_testPidFilePath, " 12345 \n");
|
||||
|
||||
// Act
|
||||
bool result = _manager.TryReadPid(_testPidFilePath, out int pid);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result);
|
||||
Assert.AreEqual(12345, pid);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryReadPid_MissingFile_ReturnsFalse()
|
||||
{
|
||||
// Act
|
||||
bool result = _manager.TryReadPid("/nonexistent/path/file.pid", out int pid);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result);
|
||||
Assert.AreEqual(0, pid);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryReadPid_NullPath_ReturnsFalse()
|
||||
{
|
||||
// Act
|
||||
bool result = _manager.TryReadPid(null, out int pid);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result);
|
||||
Assert.AreEqual(0, pid);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryReadPid_EmptyPath_ReturnsFalse()
|
||||
{
|
||||
// Act
|
||||
bool result = _manager.TryReadPid(string.Empty, out int pid);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result);
|
||||
Assert.AreEqual(0, pid);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryReadPid_InvalidContent_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
_testPidFilePath = _manager.GetPidFilePath(59996);
|
||||
File.WriteAllText(_testPidFilePath, "not a number");
|
||||
|
||||
// Act
|
||||
bool result = _manager.TryReadPid(_testPidFilePath, out int pid);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result);
|
||||
Assert.AreEqual(0, pid);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryReadPid_ZeroPid_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
_testPidFilePath = _manager.GetPidFilePath(59995);
|
||||
File.WriteAllText(_testPidFilePath, "0");
|
||||
|
||||
// Act
|
||||
bool result = _manager.TryReadPid(_testPidFilePath, out int pid);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result, "Zero PID should be rejected");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryReadPid_NegativePid_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
_testPidFilePath = _manager.GetPidFilePath(59994);
|
||||
File.WriteAllText(_testPidFilePath, "-1");
|
||||
|
||||
// Act
|
||||
bool result = _manager.TryReadPid(_testPidFilePath, out int pid);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result, "Negative PID should be rejected");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TryGetPortFromPidFilePath Tests
|
||||
|
||||
[Test]
|
||||
public void TryGetPortFromPidFilePath_ValidPath_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
string path = "/some/path/mcp_http_8080.pid";
|
||||
|
||||
// Act
|
||||
bool result = _manager.TryGetPortFromPidFilePath(path, out int port);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result);
|
||||
Assert.AreEqual(8080, port);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryGetPortFromPidFilePath_DifferentPort_ParsesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
string path = "/path/to/mcp_http_9999.pid";
|
||||
|
||||
// Act
|
||||
bool result = _manager.TryGetPortFromPidFilePath(path, out int port);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result);
|
||||
Assert.AreEqual(9999, port);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryGetPortFromPidFilePath_NullPath_ReturnsFalse()
|
||||
{
|
||||
// Act
|
||||
bool result = _manager.TryGetPortFromPidFilePath(null, out int port);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result);
|
||||
Assert.AreEqual(0, port);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryGetPortFromPidFilePath_InvalidPrefix_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
string path = "/some/path/wrong_prefix_8080.pid";
|
||||
|
||||
// Act
|
||||
bool result = _manager.TryGetPortFromPidFilePath(path, out int port);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Handshake Tests
|
||||
|
||||
[Test]
|
||||
public void StoreHandshake_ValidData_StoresInEditorPrefs()
|
||||
{
|
||||
// Arrange
|
||||
string pidFilePath = "/test/path.pid";
|
||||
string instanceToken = "test-token-123";
|
||||
|
||||
// Act
|
||||
_manager.StoreHandshake(pidFilePath, instanceToken);
|
||||
bool result = _manager.TryGetHandshake(out var storedPath, out var storedToken);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result);
|
||||
Assert.AreEqual(pidFilePath, storedPath);
|
||||
Assert.AreEqual(instanceToken, storedToken);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryGetHandshake_NoHandshake_ReturnsFalse()
|
||||
{
|
||||
// Act
|
||||
bool result = _manager.TryGetHandshake(out var pidFilePath, out var instanceToken);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result);
|
||||
Assert.IsNull(pidFilePath);
|
||||
Assert.IsNull(instanceToken);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StoreHandshake_NullValues_DoesNotThrow()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.DoesNotThrow(() =>
|
||||
{
|
||||
_manager.StoreHandshake(null, null);
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Tracking Tests
|
||||
|
||||
[Test]
|
||||
public void StoreTracking_ValidData_CanBeRetrieved()
|
||||
{
|
||||
// Arrange
|
||||
int pid = 12345;
|
||||
int port = 8080;
|
||||
|
||||
// Act
|
||||
_manager.StoreTracking(pid, port);
|
||||
bool result = _manager.TryGetStoredPid(port, out int storedPid);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result);
|
||||
Assert.AreEqual(pid, storedPid);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryGetStoredPid_WrongPort_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
_manager.StoreTracking(12345, 8080);
|
||||
|
||||
// Act
|
||||
bool result = _manager.TryGetStoredPid(9090, out int storedPid);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result, "Should return false for wrong port");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryGetStoredPid_NoTracking_ReturnsFalse()
|
||||
{
|
||||
// Act
|
||||
bool result = _manager.TryGetStoredPid(8080, out int storedPid);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result);
|
||||
Assert.AreEqual(0, storedPid);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ClearTracking_RemovesAllKeys()
|
||||
{
|
||||
// Arrange
|
||||
_manager.StoreTracking(12345, 8080, "somehash");
|
||||
_manager.StoreHandshake("/path.pid", "token");
|
||||
|
||||
// Act
|
||||
_manager.ClearTracking();
|
||||
bool hasTracking = _manager.TryGetStoredPid(8080, out _);
|
||||
bool hasHandshake = _manager.TryGetHandshake(out _, out _);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(hasTracking);
|
||||
Assert.IsFalse(hasHandshake);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetStoredArgsHash_WithHash_ReturnsHash()
|
||||
{
|
||||
// Arrange
|
||||
_manager.StoreTracking(12345, 8080, "testhash123");
|
||||
|
||||
// Act
|
||||
string hash = _manager.GetStoredArgsHash();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("testhash123", hash);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetStoredArgsHash_NoHash_ReturnsEmpty()
|
||||
{
|
||||
// Act
|
||||
string hash = _manager.GetStoredArgsHash();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(string.Empty, hash);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ComputeShortHash Tests
|
||||
|
||||
[Test]
|
||||
public void ComputeShortHash_ValidInput_Returns16CharHash()
|
||||
{
|
||||
// Arrange
|
||||
string input = "test input string";
|
||||
|
||||
// Act
|
||||
string hash = _manager.ComputeShortHash(input);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(hash);
|
||||
Assert.AreEqual(16, hash.Length);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ComputeShortHash_SameInput_ReturnsSameHash()
|
||||
{
|
||||
// Arrange
|
||||
string input = "consistent input";
|
||||
|
||||
// Act
|
||||
string hash1 = _manager.ComputeShortHash(input);
|
||||
string hash2 = _manager.ComputeShortHash(input);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(hash1, hash2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ComputeShortHash_DifferentInput_ReturnsDifferentHash()
|
||||
{
|
||||
// Act
|
||||
string hash1 = _manager.ComputeShortHash("input1");
|
||||
string hash2 = _manager.ComputeShortHash("input2");
|
||||
|
||||
// Assert
|
||||
Assert.AreNotEqual(hash1, hash2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ComputeShortHash_NullInput_ReturnsEmpty()
|
||||
{
|
||||
// Act
|
||||
string hash = _manager.ComputeShortHash(null);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(string.Empty, hash);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ComputeShortHash_EmptyInput_ReturnsEmpty()
|
||||
{
|
||||
// Act
|
||||
string hash = _manager.ComputeShortHash(string.Empty);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(string.Empty, hash);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DeletePidFile Tests
|
||||
|
||||
[Test]
|
||||
public void DeletePidFile_ExistingFile_DeletesFile()
|
||||
{
|
||||
// Arrange
|
||||
_testPidFilePath = _manager.GetPidFilePath(59993);
|
||||
File.WriteAllText(_testPidFilePath, "12345");
|
||||
Assert.IsTrue(File.Exists(_testPidFilePath));
|
||||
|
||||
// Act
|
||||
_manager.DeletePidFile(_testPidFilePath);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(File.Exists(_testPidFilePath));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DeletePidFile_NonExistentFile_DoesNotThrow()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.DoesNotThrow(() =>
|
||||
{
|
||||
_manager.DeletePidFile("/nonexistent/file.pid");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DeletePidFile_NullPath_DoesNotThrow()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.DoesNotThrow(() =>
|
||||
{
|
||||
_manager.DeletePidFile(null);
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Interface Implementation Tests
|
||||
|
||||
[Test]
|
||||
public void PidFileManager_ImplementsIPidFileManager()
|
||||
{
|
||||
// Assert
|
||||
Assert.IsInstanceOf<IPidFileManager>(_manager);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 875ef370082bc42d182a9875ee7c5e15
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+364
@@ -0,0 +1,364 @@
|
||||
using NUnit.Framework;
|
||||
using MCPForUnity.Editor.Services.Server;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Services.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Unit tests for ProcessDetector component.
|
||||
/// These tests execute subprocess commands (ps, lsof, tasklist, wmic) which can be slow.
|
||||
/// Marked as [Explicit] to exclude from normal test runs.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[Explicit]
|
||||
public class ProcessDetectorTests
|
||||
{
|
||||
private ProcessDetector _detector;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_detector = new ProcessDetector();
|
||||
}
|
||||
|
||||
#region NormalizeForMatch Tests
|
||||
|
||||
[Test]
|
||||
public void NormalizeForMatch_RemovesWhitespace()
|
||||
{
|
||||
// Arrange
|
||||
string input = "Hello World";
|
||||
|
||||
// Act
|
||||
string result = _detector.NormalizeForMatch(input);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("helloworld", result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NormalizeForMatch_LowercasesInput()
|
||||
{
|
||||
// Arrange
|
||||
string input = "UPPERCASE";
|
||||
|
||||
// Act
|
||||
string result = _detector.NormalizeForMatch(input);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("uppercase", result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NormalizeForMatch_HandlesNull()
|
||||
{
|
||||
// Act
|
||||
string result = _detector.NormalizeForMatch(null);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(string.Empty, result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NormalizeForMatch_HandlesEmptyString()
|
||||
{
|
||||
// Act
|
||||
string result = _detector.NormalizeForMatch(string.Empty);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(string.Empty, result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NormalizeForMatch_RemovesTabs()
|
||||
{
|
||||
// Arrange
|
||||
string input = "hello\tworld";
|
||||
|
||||
// Act
|
||||
string result = _detector.NormalizeForMatch(input);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("helloworld", result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NormalizeForMatch_RemovesNewlines()
|
||||
{
|
||||
// Arrange
|
||||
string input = "hello\nworld\r\ntest";
|
||||
|
||||
// Act
|
||||
string result = _detector.NormalizeForMatch(input);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("helloworldtest", result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NormalizeForMatch_PreservesNonWhitespace()
|
||||
{
|
||||
// Arrange
|
||||
string input = "mcp-for-unity_test123";
|
||||
|
||||
// Act
|
||||
string result = _detector.NormalizeForMatch(input);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("mcp-for-unity_test123", result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetCurrentProcessId Tests
|
||||
|
||||
[Test]
|
||||
public void GetCurrentProcessId_ReturnsPositiveInt()
|
||||
{
|
||||
// Act
|
||||
int pid = _detector.GetCurrentProcessId();
|
||||
|
||||
// Assert
|
||||
Assert.Greater(pid, 0, "Process ID should be positive");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetCurrentProcessId_ReturnsConsistentValue()
|
||||
{
|
||||
// Act
|
||||
int pid1 = _detector.GetCurrentProcessId();
|
||||
int pid2 = _detector.GetCurrentProcessId();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(pid1, pid2, "Process ID should be consistent across calls");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ProcessExists Tests
|
||||
|
||||
[Test]
|
||||
public void ProcessExists_CurrentProcess_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
int currentPid = _detector.GetCurrentProcessId();
|
||||
|
||||
// Act
|
||||
bool exists = _detector.ProcessExists(currentPid);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(exists, "Current process should exist");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ProcessExists_InvalidPid_ReturnsFalseOrHandlesGracefully()
|
||||
{
|
||||
// Act - Use a very high PID unlikely to exist
|
||||
bool exists = _detector.ProcessExists(9999999);
|
||||
|
||||
// Assert - Should not throw, may return false or true (assumes exists if cannot verify)
|
||||
Assert.Pass($"ProcessExists returned {exists} for invalid PID (handles gracefully)");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ProcessExists_ZeroPid_HandlesGracefully()
|
||||
{
|
||||
// Act & Assert - Should not throw
|
||||
Assert.DoesNotThrow(() =>
|
||||
{
|
||||
_detector.ProcessExists(0);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ProcessExists_NegativePid_HandlesGracefully()
|
||||
{
|
||||
// Act & Assert - Should not throw
|
||||
Assert.DoesNotThrow(() =>
|
||||
{
|
||||
_detector.ProcessExists(-1);
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetListeningProcessIdsForPort Tests
|
||||
|
||||
[Test]
|
||||
public void GetListeningProcessIdsForPort_InvalidPort_ReturnsEmpty()
|
||||
{
|
||||
// Act
|
||||
var pids = _detector.GetListeningProcessIdsForPort(-1);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(pids);
|
||||
Assert.IsEmpty(pids, "Invalid port should return empty list");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetListeningProcessIdsForPort_UnusedPort_ReturnsEmpty()
|
||||
{
|
||||
// Act - Use a port that's unlikely to be in use
|
||||
var pids = _detector.GetListeningProcessIdsForPort(59999);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(pids);
|
||||
Assert.IsEmpty(pids, "Unused port should return empty list");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetListeningProcessIdsForPort_ReturnsDistinctPids()
|
||||
{
|
||||
// Act
|
||||
var pids = _detector.GetListeningProcessIdsForPort(80);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(pids);
|
||||
CollectionAssert.AllItemsAreUnique(pids, "PIDs should be distinct");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetListeningProcessIdsForPort_DoesNotThrow()
|
||||
{
|
||||
// Act & Assert - Should handle any port gracefully
|
||||
Assert.DoesNotThrow(() =>
|
||||
{
|
||||
_detector.GetListeningProcessIdsForPort(8080);
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TryGetProcessCommandLine Tests
|
||||
|
||||
[Test]
|
||||
public void TryGetProcessCommandLine_CurrentProcess_ReturnsResult()
|
||||
{
|
||||
// Arrange
|
||||
int currentPid = _detector.GetCurrentProcessId();
|
||||
|
||||
// Act
|
||||
bool result = _detector.TryGetProcessCommandLine(currentPid, out string argsLower);
|
||||
|
||||
// Assert - Platform dependent, but should not throw
|
||||
Assert.Pass($"TryGetProcessCommandLine: success={result}, argsLower length={argsLower?.Length ?? 0}");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryGetProcessCommandLine_InvalidPid_ReturnsFalse()
|
||||
{
|
||||
// Act
|
||||
bool result = _detector.TryGetProcessCommandLine(9999999, out string argsLower);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result, "Invalid PID should return false");
|
||||
Assert.IsEmpty(argsLower, "Args should be empty for invalid PID");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryGetProcessCommandLine_ReturnsNormalizedOutput()
|
||||
{
|
||||
// Arrange
|
||||
int currentPid = _detector.GetCurrentProcessId();
|
||||
|
||||
// Act
|
||||
bool result = _detector.TryGetProcessCommandLine(currentPid, out string argsLower);
|
||||
|
||||
// Assert
|
||||
if (result && !string.IsNullOrEmpty(argsLower))
|
||||
{
|
||||
// Verify output is normalized (no whitespace, lowercase)
|
||||
Assert.IsFalse(argsLower.Contains(" "), "Output should have no spaces");
|
||||
Assert.AreEqual(argsLower, argsLower.ToLowerInvariant(), "Output should be lowercase");
|
||||
}
|
||||
Assert.Pass("Command line is properly normalized");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region LooksLikeMcpServerProcess Tests
|
||||
|
||||
[Test]
|
||||
public void LooksLikeMcpServerProcess_CurrentProcess_ReturnsFalse()
|
||||
{
|
||||
// Arrange - Unity Editor process should not be an MCP server
|
||||
int currentPid = _detector.GetCurrentProcessId();
|
||||
|
||||
// Act
|
||||
bool result = _detector.LooksLikeMcpServerProcess(currentPid);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result, "Unity Editor should not be identified as MCP server");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LooksLikeMcpServerProcess_InvalidPid_ReturnsFalse()
|
||||
{
|
||||
// Act
|
||||
bool result = _detector.LooksLikeMcpServerProcess(9999999);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result, "Invalid PID should return false");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LooksLikeMcpServerProcess_ZeroPid_ReturnsFalse()
|
||||
{
|
||||
// Act
|
||||
bool result = _detector.LooksLikeMcpServerProcess(0);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result, "Zero PID should return false");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LooksLikeMcpServerProcess_NegativePid_ReturnsFalse()
|
||||
{
|
||||
// Act
|
||||
bool result = _detector.LooksLikeMcpServerProcess(-1);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result, "Negative PID should return false");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LooksLikeMcpServerProcess_DoesNotThrow()
|
||||
{
|
||||
// Act & Assert - Should handle any PID gracefully
|
||||
Assert.DoesNotThrow(() =>
|
||||
{
|
||||
_detector.LooksLikeMcpServerProcess(12345);
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Interface Implementation Tests
|
||||
|
||||
[Test]
|
||||
public void ProcessDetector_ImplementsIProcessDetector()
|
||||
{
|
||||
// Assert
|
||||
Assert.IsInstanceOf<IProcessDetector>(_detector);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ProcessDetector_CanBeUsedViaInterface()
|
||||
{
|
||||
// Arrange
|
||||
IProcessDetector detector = new ProcessDetector();
|
||||
|
||||
// Act & Assert - All interface methods should work
|
||||
Assert.DoesNotThrow(() =>
|
||||
{
|
||||
detector.NormalizeForMatch("test");
|
||||
detector.GetCurrentProcessId();
|
||||
detector.ProcessExists(1);
|
||||
detector.GetListeningProcessIdsForPort(8080);
|
||||
detector.TryGetProcessCommandLine(1, out _);
|
||||
detector.LooksLikeMcpServerProcess(1);
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 88e47e156fb6a4064a2d638cf8bb8d52
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
using NUnit.Framework;
|
||||
using MCPForUnity.Editor.Services.Server;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Services.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Unit tests for ProcessTerminator component.
|
||||
/// Note: Most tests avoid actually terminating processes to prevent test instability.
|
||||
/// Uses ProcessDetector which executes subprocess commands (ps, tasklist, etc.), so marked as [Explicit].
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[Explicit]
|
||||
public class ProcessTerminatorTests
|
||||
{
|
||||
private ProcessTerminator _terminator;
|
||||
private ProcessDetector _detector;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_detector = new ProcessDetector();
|
||||
_terminator = new ProcessTerminator(_detector);
|
||||
}
|
||||
|
||||
#region Constructor Tests
|
||||
|
||||
[Test]
|
||||
public void Constructor_NullDetector_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<System.ArgumentNullException>(() =>
|
||||
{
|
||||
new ProcessTerminator(null);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Constructor_ValidDetector_Succeeds()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.DoesNotThrow(() =>
|
||||
{
|
||||
new ProcessTerminator(_detector);
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Terminate Tests
|
||||
|
||||
[Test]
|
||||
public void Terminate_InvalidPid_ReturnsFalse()
|
||||
{
|
||||
// Act
|
||||
bool result = _terminator.Terminate(-1);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result, "Invalid PID should fail to terminate");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Terminate_ZeroPid_ReturnsFalse()
|
||||
{
|
||||
// Act
|
||||
bool result = _terminator.Terminate(0);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result, "Zero PID should fail to terminate");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Terminate_Pid1_ReturnsFalse()
|
||||
{
|
||||
// PID 1 is init/launchd and must never be killed
|
||||
// Act
|
||||
bool result = _terminator.Terminate(1);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result, "PID 1 (init/launchd) should never be terminated");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Terminate_CurrentProcessPid_ReturnsFalse()
|
||||
{
|
||||
// Should never kill the Unity Editor process
|
||||
// Act
|
||||
int currentPid = _detector.GetCurrentProcessId();
|
||||
bool result = _terminator.Terminate(currentPid);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result, "Current process PID should never be terminated");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Terminate_NonExistentPid_ReturnsFalseOrHandlesGracefully()
|
||||
{
|
||||
// Act - Use a very high PID unlikely to exist
|
||||
bool result = _terminator.Terminate(9999999);
|
||||
|
||||
// Assert - Should not terminate non-existent PID
|
||||
Assert.IsFalse(result, $"Terminate returned {result} for non-existent PID");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Terminate_DoesNotThrow()
|
||||
{
|
||||
// Act & Assert - Should handle any PID gracefully
|
||||
Assert.DoesNotThrow(() =>
|
||||
{
|
||||
_terminator.Terminate(int.MaxValue);
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Interface Implementation Tests
|
||||
|
||||
[Test]
|
||||
public void ProcessTerminator_ImplementsIProcessTerminator()
|
||||
{
|
||||
// Assert
|
||||
Assert.IsInstanceOf<IProcessTerminator>(_terminator);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ProcessTerminator_CanBeUsedViaInterface()
|
||||
{
|
||||
// Arrange
|
||||
IProcessTerminator terminator = new ProcessTerminator(_detector);
|
||||
|
||||
// Act & Assert - Should be callable via interface
|
||||
Assert.DoesNotThrow(() =>
|
||||
{
|
||||
// Don't actually terminate anything
|
||||
terminator.Terminate(-1);
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Integration Tests (with real detector)
|
||||
|
||||
[Test]
|
||||
public void Terminate_WithRealDetector_HandlesMissingProcess()
|
||||
{
|
||||
// Arrange
|
||||
var realDetector = new ProcessDetector();
|
||||
var terminator = new ProcessTerminator(realDetector);
|
||||
|
||||
// Act - Try to terminate a PID that definitely doesn't exist
|
||||
bool result = terminator.Terminate(int.MaxValue);
|
||||
|
||||
// Assert - Should return false without throwing
|
||||
Assert.IsFalse(result, "Terminating non-existent process should return false");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 48fe9f588d61e46319110ad1c09d0aea
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+325
@@ -0,0 +1,325 @@
|
||||
using NUnit.Framework;
|
||||
using MCPForUnity.Editor.Services;
|
||||
using MCPForUnity.Editor.Services.Server;
|
||||
using MCPForUnity.Editor.Constants;
|
||||
using UnityEditor;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Services.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Unit tests for ServerCommandBuilder component.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class ServerCommandBuilderTests
|
||||
{
|
||||
private ServerCommandBuilder _builder;
|
||||
private bool _savedUseHttpTransport;
|
||||
private string _savedHttpUrl;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_builder = new ServerCommandBuilder();
|
||||
// Save current settings
|
||||
_savedUseHttpTransport = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true);
|
||||
_savedHttpUrl = EditorPrefs.GetString(EditorPrefKeys.HttpBaseUrl, string.Empty);
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
// Restore settings
|
||||
EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, _savedUseHttpTransport);
|
||||
if (!string.IsNullOrEmpty(_savedHttpUrl))
|
||||
{
|
||||
EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, _savedHttpUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorPrefs.DeleteKey(EditorPrefKeys.HttpBaseUrl);
|
||||
}
|
||||
// Refresh cache to reflect restored values
|
||||
EditorConfigurationCache.Instance.Refresh();
|
||||
}
|
||||
|
||||
#region QuoteIfNeeded Tests
|
||||
|
||||
[Test]
|
||||
public void QuoteIfNeeded_PathWithSpaces_AddsQuotes()
|
||||
{
|
||||
// Arrange
|
||||
string input = "path with spaces";
|
||||
|
||||
// Act
|
||||
string result = _builder.QuoteIfNeeded(input);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("\"path with spaces\"", result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void QuoteIfNeeded_PathWithoutSpaces_NoChange()
|
||||
{
|
||||
// Arrange
|
||||
string input = "pathwithoutspaces";
|
||||
|
||||
// Act
|
||||
string result = _builder.QuoteIfNeeded(input);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("pathwithoutspaces", result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void QuoteIfNeeded_NullInput_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
string result = _builder.QuoteIfNeeded(null);
|
||||
|
||||
// Assert
|
||||
Assert.IsNull(result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void QuoteIfNeeded_EmptyInput_ReturnsEmpty()
|
||||
{
|
||||
// Act
|
||||
string result = _builder.QuoteIfNeeded(string.Empty);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(string.Empty, result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void QuoteIfNeeded_AlreadyQuoted_AddsMoreQuotes()
|
||||
{
|
||||
// Arrange - This is intentional behavior - don't double-escape
|
||||
string input = "\"already quoted\"";
|
||||
|
||||
// Act
|
||||
string result = _builder.QuoteIfNeeded(input);
|
||||
|
||||
// Assert - Has spaces so gets quoted
|
||||
Assert.AreEqual("\"\"already quoted\"\"", result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region BuildUvPathFromUvx Tests
|
||||
|
||||
[Test]
|
||||
public void BuildUvPathFromUvx_ValidPath_ConvertsCorrectly()
|
||||
{
|
||||
// This test uses Unix-style paths which only work correctly on non-Windows
|
||||
if (UnityEngine.Application.platform == UnityEngine.RuntimePlatform.WindowsEditor)
|
||||
{
|
||||
Assert.Pass("Skipped on Windows - use BuildUvPathFromUvx_WindowsPath_ConvertsCorrectly instead");
|
||||
return;
|
||||
}
|
||||
|
||||
// Arrange
|
||||
string uvxPath = "/usr/local/bin/uvx";
|
||||
|
||||
// Act
|
||||
string result = _builder.BuildUvPathFromUvx(uvxPath);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("/usr/local/bin/uv", result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BuildUvPathFromUvx_WindowsPath_ConvertsCorrectly()
|
||||
{
|
||||
// This test only makes sense on Windows where backslash paths are native
|
||||
if (UnityEngine.Application.platform != UnityEngine.RuntimePlatform.WindowsEditor)
|
||||
{
|
||||
Assert.Pass("Skipped on non-Windows platform");
|
||||
return;
|
||||
}
|
||||
|
||||
// Arrange
|
||||
string uvxPath = @"C:\Program Files\uv\uvx.exe";
|
||||
|
||||
// Act
|
||||
string result = _builder.BuildUvPathFromUvx(uvxPath);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(@"C:\Program Files\uv\uv.exe", result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BuildUvPathFromUvx_NullPath_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
string result = _builder.BuildUvPathFromUvx(null);
|
||||
|
||||
// Assert
|
||||
Assert.IsNull(result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BuildUvPathFromUvx_EmptyPath_ReturnsEmpty()
|
||||
{
|
||||
// Act
|
||||
string result = _builder.BuildUvPathFromUvx(string.Empty);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(string.Empty, result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BuildUvPathFromUvx_WhitespacePath_ReturnsWhitespace()
|
||||
{
|
||||
// Act
|
||||
string result = _builder.BuildUvPathFromUvx(" ");
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(" ", result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BuildUvPathFromUvx_JustFilename_ConvertsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
string uvxPath = "uvx";
|
||||
|
||||
// Act
|
||||
string result = _builder.BuildUvPathFromUvx(uvxPath);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("uv", result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetPlatformSpecificPathPrepend Tests
|
||||
|
||||
[Test]
|
||||
public void GetPlatformSpecificPathPrepend_ReturnsNonNull()
|
||||
{
|
||||
// Act
|
||||
string result = _builder.GetPlatformSpecificPathPrepend();
|
||||
|
||||
// Assert - May be null on some platforms, but should not throw
|
||||
Assert.Pass($"GetPlatformSpecificPathPrepend returned: {result ?? "null"}");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetPlatformSpecificPathPrepend_DoesNotThrow()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.DoesNotThrow(() =>
|
||||
{
|
||||
_builder.GetPlatformSpecificPathPrepend();
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TryBuildCommand Tests
|
||||
|
||||
[Test]
|
||||
public void TryBuildCommand_HttpDisabled_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, false);
|
||||
EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://localhost:8080");
|
||||
EditorConfigurationCache.Instance.Refresh();
|
||||
|
||||
// Act
|
||||
bool result = _builder.TryBuildCommand(out string fileName, out string arguments, out string displayCommand, out string error);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result);
|
||||
Assert.IsNull(fileName);
|
||||
Assert.IsNull(arguments);
|
||||
Assert.IsNull(displayCommand);
|
||||
Assert.IsNotNull(error);
|
||||
Assert.That(error, Does.Contain("HTTP").IgnoreCase);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryBuildCommand_RemoteUrl_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, true);
|
||||
EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://remote.server.com:8080");
|
||||
EditorConfigurationCache.Instance.Refresh();
|
||||
|
||||
// Act
|
||||
bool result = _builder.TryBuildCommand(out string fileName, out string arguments, out string displayCommand, out string error);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result);
|
||||
Assert.IsNotNull(error);
|
||||
Assert.That(error, Does.Contain("local").IgnoreCase);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryBuildCommand_LocalUrl_ReturnsCommandOrError()
|
||||
{
|
||||
// Arrange
|
||||
EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, true);
|
||||
EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://localhost:8080");
|
||||
EditorConfigurationCache.Instance.Refresh();
|
||||
|
||||
// Act
|
||||
bool result = _builder.TryBuildCommand(out string fileName, out string arguments, out string displayCommand, out string error);
|
||||
|
||||
// Assert - Success depends on uvx availability
|
||||
if (result)
|
||||
{
|
||||
Assert.IsNotNull(fileName, "fileName should be set on success");
|
||||
Assert.IsNotNull(arguments, "arguments should be set on success");
|
||||
Assert.IsNotNull(displayCommand, "displayCommand should be set on success");
|
||||
Assert.IsNull(error, "error should be null on success");
|
||||
Assert.That(displayCommand, Does.Contain("uvx").Or.Contain("uv"));
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.IsNotNull(error, "error message should be provided on failure");
|
||||
}
|
||||
|
||||
Assert.Pass($"TryBuildCommand: success={result}, error={error ?? "null"}");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryBuildCommand_DoesNotThrow()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.DoesNotThrow(() =>
|
||||
{
|
||||
_builder.TryBuildCommand(out _, out _, out _, out _);
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Interface Implementation Tests
|
||||
|
||||
[Test]
|
||||
public void ServerCommandBuilder_ImplementsIServerCommandBuilder()
|
||||
{
|
||||
// Assert
|
||||
Assert.IsInstanceOf<IServerCommandBuilder>(_builder);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ServerCommandBuilder_CanBeUsedViaInterface()
|
||||
{
|
||||
// Arrange
|
||||
IServerCommandBuilder builder = new ServerCommandBuilder();
|
||||
|
||||
// Act & Assert - All interface methods should work
|
||||
Assert.DoesNotThrow(() =>
|
||||
{
|
||||
builder.QuoteIfNeeded("test");
|
||||
builder.BuildUvPathFromUvx("uvx");
|
||||
builder.GetPlatformSpecificPathPrepend();
|
||||
builder.TryBuildCommand(out _, out _, out _, out _);
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5506606af081a4eaca6a5563e1a4b0dd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using MCPForUnity.Editor.Services.Server;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Services.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Unit tests for TerminalLauncher component.
|
||||
/// Note: Tests avoid actually launching terminals to prevent test instability.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class TerminalLauncherTests
|
||||
{
|
||||
private TerminalLauncher _launcher;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_launcher = new TerminalLauncher();
|
||||
}
|
||||
|
||||
#region GetProjectRootPath Tests
|
||||
|
||||
[Test]
|
||||
public void GetProjectRootPath_ReturnsNonEmpty()
|
||||
{
|
||||
// Act
|
||||
string path = _launcher.GetProjectRootPath();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(path);
|
||||
Assert.IsNotEmpty(path);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetProjectRootPath_ReturnsValidDirectory()
|
||||
{
|
||||
// Act
|
||||
string path = _launcher.GetProjectRootPath();
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(System.IO.Directory.Exists(path), $"Project root path should exist: {path}");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetProjectRootPath_DoesNotContainAssets()
|
||||
{
|
||||
// Act
|
||||
string path = _launcher.GetProjectRootPath();
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(path.EndsWith("Assets"), "Project root should not end with Assets");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CreateTerminalProcessStartInfo Tests
|
||||
|
||||
[Test]
|
||||
public void CreateTerminalProcessStartInfo_EmptyCommand_ThrowsArgumentException()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
{
|
||||
_launcher.CreateTerminalProcessStartInfo(string.Empty);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateTerminalProcessStartInfo_NullCommand_ThrowsArgumentException()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
{
|
||||
_launcher.CreateTerminalProcessStartInfo(null);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateTerminalProcessStartInfo_WhitespaceCommand_ThrowsArgumentException()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
{
|
||||
_launcher.CreateTerminalProcessStartInfo(" ");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateTerminalProcessStartInfo_ValidCommand_ReturnsStartInfo()
|
||||
{
|
||||
// Act
|
||||
var startInfo = _launcher.CreateTerminalProcessStartInfo("echo hello");
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(startInfo);
|
||||
Assert.IsNotNull(startInfo.FileName);
|
||||
Assert.IsNotEmpty(startInfo.FileName);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateTerminalProcessStartInfo_ValidCommand_SetsUseShellExecuteFalse()
|
||||
{
|
||||
// Act
|
||||
var startInfo = _launcher.CreateTerminalProcessStartInfo("echo hello");
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(startInfo.UseShellExecute, "UseShellExecute should be false");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateTerminalProcessStartInfo_ValidCommand_SetsCreateNoWindowTrue()
|
||||
{
|
||||
// Act
|
||||
var startInfo = _launcher.CreateTerminalProcessStartInfo("echo hello");
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(startInfo.CreateNoWindow, "CreateNoWindow should be true");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateTerminalProcessStartInfo_CommandWithNewlines_StripsNewlines()
|
||||
{
|
||||
// Act - Should not throw
|
||||
var startInfo = _launcher.CreateTerminalProcessStartInfo("echo\nhello\r\nworld");
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(startInfo);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateTerminalProcessStartInfo_LongCommand_HandlesGracefully()
|
||||
{
|
||||
// Arrange
|
||||
string longCommand = new string('a', 1000);
|
||||
|
||||
// Act
|
||||
var startInfo = _launcher.CreateTerminalProcessStartInfo(longCommand);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(startInfo);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateTerminalProcessStartInfo_SpecialCharacters_HandlesGracefully()
|
||||
{
|
||||
// Arrange
|
||||
string command = "echo \"hello world\" && echo 'test' | cat";
|
||||
|
||||
// Act
|
||||
var startInfo = _launcher.CreateTerminalProcessStartInfo(command);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(startInfo);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CreateHeadlessProcessStartInfo Tests
|
||||
|
||||
[Test]
|
||||
public void CreateHeadlessProcessStartInfo_EmptyCommand_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
_launcher.CreateHeadlessProcessStartInfo(string.Empty, "/tmp/log.txt"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateHeadlessProcessStartInfo_EmptyLogPath_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
_launcher.CreateHeadlessProcessStartInfo("echo hello", string.Empty));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateHeadlessProcessStartInfo_IsHiddenNoWindow()
|
||||
{
|
||||
var startInfo = _launcher.CreateHeadlessProcessStartInfo("echo hello", LogPath());
|
||||
|
||||
Assert.IsFalse(startInfo.UseShellExecute, "UseShellExecute should be false for headless launch");
|
||||
Assert.IsTrue(startInfo.CreateNoWindow, "CreateNoWindow should be true for headless launch");
|
||||
Assert.AreEqual(System.Diagnostics.ProcessWindowStyle.Hidden, startInfo.WindowStyle,
|
||||
"WindowStyle should be Hidden for headless launch");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateHeadlessProcessStartInfo_DoesNotOpenTerminal()
|
||||
{
|
||||
var startInfo = _launcher.CreateHeadlessProcessStartInfo("echo hello", LogPath());
|
||||
|
||||
// Must NOT route through Terminal.app / start / a terminal emulator.
|
||||
#if UNITY_EDITOR_WIN
|
||||
Assert.AreEqual("cmd.exe", startInfo.FileName, "Windows headless should run via cmd.exe");
|
||||
StringAssert.DoesNotContain("start ", startInfo.Arguments, "Windows headless must not use 'start' (new window)");
|
||||
#else
|
||||
Assert.AreEqual("/bin/bash", startInfo.FileName, "macOS/Linux headless should run via /bin/bash");
|
||||
StringAssert.DoesNotContain("open", startInfo.Arguments, "macOS headless must not use 'open -a Terminal'");
|
||||
StringAssert.DoesNotContain("Terminal", startInfo.Arguments, "macOS headless must not open Terminal.app");
|
||||
#endif
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateHeadlessProcessStartInfo_RedirectsOutputToLogFile()
|
||||
{
|
||||
string logPath = LogPath();
|
||||
|
||||
var startInfo = _launcher.CreateHeadlessProcessStartInfo("echo hello", logPath);
|
||||
|
||||
// The redirect to the log file is part of the shell payload.
|
||||
StringAssert.Contains(logPath, startInfo.Arguments, "Arguments should reference the log file path");
|
||||
StringAssert.Contains("2>&1", startInfo.Arguments, "stderr should be redirected to stdout (and the log)");
|
||||
StringAssert.Contains(">>", startInfo.Arguments, "output should be appended to the log via >>");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateHeadlessProcessStartInfo_LogPathWithSpaces_IsQuoted()
|
||||
{
|
||||
// A log path containing spaces must remain a single token.
|
||||
string logPath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "Mcp Logs", "server launch.log");
|
||||
|
||||
var startInfo = _launcher.CreateHeadlessProcessStartInfo("uvx run-server", logPath);
|
||||
|
||||
StringAssert.Contains(logPath, startInfo.Arguments, "Arguments should contain the full spaced log path");
|
||||
#if UNITY_EDITOR_WIN
|
||||
StringAssert.Contains($"\"{logPath}\"", startInfo.Arguments, "Windows should double-quote a spaced log path");
|
||||
#else
|
||||
StringAssert.Contains($"'{logPath}'", startInfo.Arguments, "macOS/Linux should single-quote a spaced log path");
|
||||
#endif
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateHeadlessProcessStartInfo_CommandWithSpaces_Preserved()
|
||||
{
|
||||
string command = "/path with spaces/uvx --no-cache run mcp-for-unity";
|
||||
|
||||
var startInfo = _launcher.CreateHeadlessProcessStartInfo(command, LogPath());
|
||||
|
||||
StringAssert.Contains(command, startInfo.Arguments, "The command (incl. spaces) should be preserved in Arguments");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateHeadlessProcessStartInfo_StripsNewlines()
|
||||
{
|
||||
var startInfo = _launcher.CreateHeadlessProcessStartInfo("echo\nhello\r\nworld", LogPath());
|
||||
|
||||
Assert.IsNotNull(startInfo);
|
||||
StringAssert.DoesNotContain("\n", startInfo.Arguments);
|
||||
StringAssert.DoesNotContain("\r", startInfo.Arguments);
|
||||
}
|
||||
|
||||
private static string LogPath()
|
||||
{
|
||||
return System.IO.Path.Combine(System.IO.Path.GetTempPath(), "mcp-headless-test.log");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Interface Implementation Tests
|
||||
|
||||
[Test]
|
||||
public void TerminalLauncher_ImplementsITerminalLauncher()
|
||||
{
|
||||
// Assert
|
||||
Assert.IsInstanceOf<ITerminalLauncher>(_launcher);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TerminalLauncher_CanBeUsedViaInterface()
|
||||
{
|
||||
// Arrange
|
||||
ITerminalLauncher launcher = new TerminalLauncher();
|
||||
|
||||
// Act & Assert
|
||||
Assert.DoesNotThrow(() =>
|
||||
{
|
||||
launcher.GetProjectRootPath();
|
||||
launcher.CreateTerminalProcessStartInfo("test");
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Platform-Specific Behavior Tests
|
||||
|
||||
[Test]
|
||||
public void CreateTerminalProcessStartInfo_ReturnsAppropriateTerminal()
|
||||
{
|
||||
// Act
|
||||
var startInfo = _launcher.CreateTerminalProcessStartInfo("echo test");
|
||||
|
||||
// Assert - Platform-specific
|
||||
#if UNITY_EDITOR_OSX
|
||||
Assert.AreEqual("/usr/bin/open", startInfo.FileName, "macOS should use 'open'");
|
||||
#elif UNITY_EDITOR_WIN
|
||||
Assert.AreEqual("cmd.exe", startInfo.FileName, "Windows should use 'cmd.exe'");
|
||||
#else
|
||||
// Linux uses detected terminal
|
||||
Assert.IsNotNull(startInfo.FileName, "Linux should have a terminal command");
|
||||
#endif
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 59d7c435f60554a719ddc9cd85f6ad3c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user