-
Notifications
You must be signed in to change notification settings - Fork 461
Feat/robust component resolver #256
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
Changes from all commits
c40f3d0
cb42364
aac237c
17ad011
43abb00
7156009
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
using UnityEngine; | ||
|
||
namespace TestNamespace | ||
{ | ||
public class CustomComponent : MonoBehaviour | ||
{ | ||
[SerializeField] | ||
private string customText = "Hello from custom asmdef!"; | ||
|
||
[SerializeField] | ||
private float customFloat = 42.0f; | ||
|
||
void Start() | ||
{ | ||
Debug.Log($"CustomComponent started: {customText}, value: {customFloat}"); | ||
} | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
{ | ||
"name": "TestAsmdef", | ||
"rootNamespace": "TestNamespace", | ||
"references": [], | ||
"includePlatforms": [], | ||
"excludePlatforms": [], | ||
"allowUnsafeCode": false, | ||
"overrideReferences": false, | ||
"precompiledReferences": [], | ||
"autoReferenced": false, | ||
"defineConstraints": [], | ||
"versionDefines": [], | ||
"noEngineReferences": false | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using NUnit.Framework; | ||
using UnityEngine; | ||
using MCPForUnity.Editor.Tools; | ||
using static MCPForUnity.Editor.Tools.ManageGameObject; | ||
|
||
namespace MCPForUnityTests.Editor.Tools | ||
{ | ||
public class AIPropertyMatchingTests | ||
{ | ||
private List<string> sampleProperties; | ||
|
||
[SetUp] | ||
public void SetUp() | ||
{ | ||
sampleProperties = new List<string> | ||
{ | ||
"maxReachDistance", | ||
"maxHorizontalDistance", | ||
"maxVerticalDistance", | ||
"moveSpeed", | ||
"healthPoints", | ||
"playerName", | ||
"isEnabled", | ||
"mass", | ||
"velocity", | ||
"transform" | ||
}; | ||
} | ||
|
||
[Test] | ||
public void GetAllComponentProperties_ReturnsValidProperties_ForTransform() | ||
{ | ||
var properties = ComponentResolver.GetAllComponentProperties(typeof(Transform)); | ||
|
||
Assert.IsNotEmpty(properties, "Transform should have properties"); | ||
Assert.Contains("position", properties, "Transform should have position property"); | ||
Assert.Contains("rotation", properties, "Transform should have rotation property"); | ||
Assert.Contains("localScale", properties, "Transform should have localScale property"); | ||
} | ||
|
||
[Test] | ||
public void GetAllComponentProperties_ReturnsEmpty_ForNullType() | ||
{ | ||
var properties = ComponentResolver.GetAllComponentProperties(null); | ||
|
||
Assert.IsEmpty(properties, "Null type should return empty list"); | ||
} | ||
|
||
[Test] | ||
public void GetAIPropertySuggestions_ReturnsEmpty_ForNullInput() | ||
{ | ||
var suggestions = ComponentResolver.GetAIPropertySuggestions(null, sampleProperties); | ||
|
||
Assert.IsEmpty(suggestions, "Null input should return no suggestions"); | ||
} | ||
|
||
[Test] | ||
public void GetAIPropertySuggestions_ReturnsEmpty_ForEmptyInput() | ||
{ | ||
var suggestions = ComponentResolver.GetAIPropertySuggestions("", sampleProperties); | ||
|
||
Assert.IsEmpty(suggestions, "Empty input should return no suggestions"); | ||
} | ||
|
||
[Test] | ||
public void GetAIPropertySuggestions_ReturnsEmpty_ForEmptyPropertyList() | ||
{ | ||
var suggestions = ComponentResolver.GetAIPropertySuggestions("test", new List<string>()); | ||
|
||
Assert.IsEmpty(suggestions, "Empty property list should return no suggestions"); | ||
} | ||
|
||
[Test] | ||
public void GetAIPropertySuggestions_FindsExactMatch_AfterCleaning() | ||
{ | ||
var suggestions = ComponentResolver.GetAIPropertySuggestions("Max Reach Distance", sampleProperties); | ||
|
||
Assert.Contains("maxReachDistance", suggestions, "Should find exact match after cleaning spaces"); | ||
Assert.AreEqual(1, suggestions.Count, "Should return exactly one match for exact match"); | ||
} | ||
|
||
[Test] | ||
public void GetAIPropertySuggestions_FindsMultipleWordMatches() | ||
{ | ||
var suggestions = ComponentResolver.GetAIPropertySuggestions("max distance", sampleProperties); | ||
|
||
Assert.Contains("maxReachDistance", suggestions, "Should match maxReachDistance"); | ||
Assert.Contains("maxHorizontalDistance", suggestions, "Should match maxHorizontalDistance"); | ||
Assert.Contains("maxVerticalDistance", suggestions, "Should match maxVerticalDistance"); | ||
} | ||
|
||
[Test] | ||
public void GetAIPropertySuggestions_FindsSimilarStrings_WithTypos() | ||
{ | ||
var suggestions = ComponentResolver.GetAIPropertySuggestions("movespeed", sampleProperties); // missing capital S | ||
|
||
Assert.Contains("moveSpeed", suggestions, "Should find moveSpeed despite missing capital"); | ||
} | ||
|
||
[Test] | ||
public void GetAIPropertySuggestions_FindsSemanticMatches_ForCommonTerms() | ||
{ | ||
var suggestions = ComponentResolver.GetAIPropertySuggestions("weight", sampleProperties); | ||
|
||
// Note: Current algorithm might not find "mass" but should handle it gracefully | ||
Assert.IsNotNull(suggestions, "Should return valid suggestions list"); | ||
} | ||
|
||
[Test] | ||
public void GetAIPropertySuggestions_LimitsResults_ToReasonableNumber() | ||
{ | ||
// Test with input that might match many properties | ||
var suggestions = ComponentResolver.GetAIPropertySuggestions("m", sampleProperties); | ||
|
||
Assert.LessOrEqual(suggestions.Count, 3, "Should limit suggestions to 3 or fewer"); | ||
} | ||
|
||
[Test] | ||
public void GetAIPropertySuggestions_CachesResults() | ||
{ | ||
var input = "Max Reach Distance"; | ||
|
||
// First call | ||
var suggestions1 = ComponentResolver.GetAIPropertySuggestions(input, sampleProperties); | ||
|
||
// Second call should use cache (tested indirectly by ensuring consistency) | ||
var suggestions2 = ComponentResolver.GetAIPropertySuggestions(input, sampleProperties); | ||
|
||
Assert.AreEqual(suggestions1.Count, suggestions2.Count, "Cached results should be consistent"); | ||
CollectionAssert.AreEqual(suggestions1, suggestions2, "Cached results should be identical"); | ||
} | ||
|
||
[Test] | ||
public void GetAIPropertySuggestions_HandlesUnityNamingConventions() | ||
{ | ||
var unityStyleProperties = new List<string> { "isKinematic", "useGravity", "maxLinearVelocity" }; | ||
|
||
var suggestions1 = ComponentResolver.GetAIPropertySuggestions("is kinematic", unityStyleProperties); | ||
var suggestions2 = ComponentResolver.GetAIPropertySuggestions("use gravity", unityStyleProperties); | ||
var suggestions3 = ComponentResolver.GetAIPropertySuggestions("max linear velocity", unityStyleProperties); | ||
|
||
Assert.Contains("isKinematic", suggestions1, "Should handle 'is' prefix convention"); | ||
Assert.Contains("useGravity", suggestions2, "Should handle 'use' prefix convention"); | ||
Assert.Contains("maxLinearVelocity", suggestions3, "Should handle 'max' prefix convention"); | ||
} | ||
|
||
[Test] | ||
public void GetAIPropertySuggestions_PrioritizesExactMatches() | ||
{ | ||
var properties = new List<string> { "speed", "moveSpeed", "maxSpeed", "speedMultiplier" }; | ||
var suggestions = ComponentResolver.GetAIPropertySuggestions("speed", properties); | ||
|
||
Assert.IsNotEmpty(suggestions, "Should find suggestions"); | ||
Assert.AreEqual("speed", suggestions[0], "Exact match should be prioritized first"); | ||
} | ||
|
||
[Test] | ||
public void GetAIPropertySuggestions_HandlesCaseInsensitive() | ||
{ | ||
var suggestions1 = ComponentResolver.GetAIPropertySuggestions("MAXREACHDISTANCE", sampleProperties); | ||
var suggestions2 = ComponentResolver.GetAIPropertySuggestions("maxreachdistance", sampleProperties); | ||
|
||
Assert.Contains("maxReachDistance", suggestions1, "Should handle uppercase input"); | ||
Assert.Contains("maxReachDistance", suggestions2, "Should handle lowercase input"); | ||
} | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
using System; | ||
using NUnit.Framework; | ||
using UnityEngine; | ||
using MCPForUnity.Editor.Tools; | ||
using static MCPForUnity.Editor.Tools.ManageGameObject; | ||
|
||
namespace MCPForUnityTests.Editor.Tools | ||
{ | ||
public class ComponentResolverTests | ||
{ | ||
[Test] | ||
public void TryResolve_ReturnsTrue_ForBuiltInComponentShortName() | ||
{ | ||
bool result = ComponentResolver.TryResolve("Transform", out Type type, out string error); | ||
|
||
Assert.IsTrue(result, "Should resolve Transform component"); | ||
Assert.AreEqual(typeof(Transform), type, "Should return correct Transform type"); | ||
Assert.IsEmpty(error, "Should have no error message"); | ||
} | ||
|
||
[Test] | ||
public void TryResolve_ReturnsTrue_ForBuiltInComponentFullyQualifiedName() | ||
{ | ||
bool result = ComponentResolver.TryResolve("UnityEngine.Rigidbody", out Type type, out string error); | ||
|
||
Assert.IsTrue(result, "Should resolve UnityEngine.Rigidbody component"); | ||
Assert.AreEqual(typeof(Rigidbody), type, "Should return correct Rigidbody type"); | ||
Assert.IsEmpty(error, "Should have no error message"); | ||
} | ||
|
||
[Test] | ||
public void TryResolve_ReturnsTrue_ForCustomComponentShortName() | ||
{ | ||
bool result = ComponentResolver.TryResolve("CustomComponent", out Type type, out string error); | ||
|
||
Assert.IsTrue(result, "Should resolve CustomComponent"); | ||
Assert.IsNotNull(type, "Should return valid type"); | ||
Assert.AreEqual("CustomComponent", type.Name, "Should have correct type name"); | ||
Assert.IsTrue(typeof(Component).IsAssignableFrom(type), "Should be a Component type"); | ||
Assert.IsEmpty(error, "Should have no error message"); | ||
} | ||
|
||
[Test] | ||
public void TryResolve_ReturnsTrue_ForCustomComponentFullyQualifiedName() | ||
{ | ||
bool result = ComponentResolver.TryResolve("TestNamespace.CustomComponent", out Type type, out string error); | ||
|
||
Assert.IsTrue(result, "Should resolve TestNamespace.CustomComponent"); | ||
Assert.IsNotNull(type, "Should return valid type"); | ||
Assert.AreEqual("CustomComponent", type.Name, "Should have correct type name"); | ||
Assert.AreEqual("TestNamespace.CustomComponent", type.FullName, "Should have correct full name"); | ||
Assert.IsTrue(typeof(Component).IsAssignableFrom(type), "Should be a Component type"); | ||
Assert.IsEmpty(error, "Should have no error message"); | ||
} | ||
|
||
[Test] | ||
public void TryResolve_ReturnsFalse_ForNonExistentComponent() | ||
{ | ||
bool result = ComponentResolver.TryResolve("NonExistentComponent", out Type type, out string error); | ||
|
||
Assert.IsFalse(result, "Should not resolve non-existent component"); | ||
Assert.IsNull(type, "Should return null type"); | ||
Assert.IsNotEmpty(error, "Should have error message"); | ||
Assert.That(error, Does.Contain("not found"), "Error should mention component not found"); | ||
} | ||
|
||
[Test] | ||
public void TryResolve_ReturnsFalse_ForEmptyString() | ||
{ | ||
bool result = ComponentResolver.TryResolve("", out Type type, out string error); | ||
|
||
Assert.IsFalse(result, "Should not resolve empty string"); | ||
Assert.IsNull(type, "Should return null type"); | ||
Assert.IsNotEmpty(error, "Should have error message"); | ||
} | ||
|
||
[Test] | ||
public void TryResolve_ReturnsFalse_ForNullString() | ||
{ | ||
bool result = ComponentResolver.TryResolve(null, out Type type, out string error); | ||
|
||
Assert.IsFalse(result, "Should not resolve null string"); | ||
Assert.IsNull(type, "Should return null type"); | ||
Assert.IsNotEmpty(error, "Should have error message"); | ||
Assert.That(error, Does.Contain("null or empty"), "Error should mention null or empty"); | ||
} | ||
|
||
[Test] | ||
public void TryResolve_CachesResolvedTypes() | ||
{ | ||
// First call | ||
bool result1 = ComponentResolver.TryResolve("Transform", out Type type1, out string error1); | ||
|
||
// Second call should use cache | ||
bool result2 = ComponentResolver.TryResolve("Transform", out Type type2, out string error2); | ||
|
||
Assert.IsTrue(result1, "First call should succeed"); | ||
Assert.IsTrue(result2, "Second call should succeed"); | ||
Assert.AreSame(type1, type2, "Should return same type instance (cached)"); | ||
Assert.IsEmpty(error1, "First call should have no error"); | ||
Assert.IsEmpty(error2, "Second call should have no error"); | ||
} | ||
|
||
[Test] | ||
public void TryResolve_PrefersPlayerAssemblies() | ||
{ | ||
// Test that custom user scripts (in Player assemblies) are found | ||
bool result = ComponentResolver.TryResolve("TicTacToe3D", out Type type, out string error); | ||
|
||
Assert.IsTrue(result, "Should resolve user script from Player assembly"); | ||
Assert.IsNotNull(type, "Should return valid type"); | ||
|
||
// Verify it's not from an Editor assembly by checking the assembly name | ||
string assemblyName = type.Assembly.GetName().Name; | ||
Assert.That(assemblyName, Does.Not.Contain("Editor"), | ||
"User script should come from Player assembly, not Editor assembly"); | ||
} | ||
|
||
[Test] | ||
public void TryResolve_HandlesDuplicateNames_WithAmbiguityError() | ||
{ | ||
// This test would need duplicate component names to be meaningful | ||
// For now, test with a built-in component that should not have duplicates | ||
bool result = ComponentResolver.TryResolve("Transform", out Type type, out string error); | ||
|
||
Assert.IsTrue(result, "Transform should resolve uniquely"); | ||
Assert.AreEqual(typeof(Transform), type, "Should return correct type"); | ||
Assert.IsEmpty(error, "Should have no ambiguity error"); | ||
} | ||
|
||
[Test] | ||
public void ResolvedType_IsValidComponent() | ||
{ | ||
bool result = ComponentResolver.TryResolve("Rigidbody", out Type type, out string error); | ||
|
||
Assert.IsTrue(result, "Should resolve Rigidbody"); | ||
Assert.IsTrue(typeof(Component).IsAssignableFrom(type), "Resolved type should be assignable from Component"); | ||
Assert.IsTrue(typeof(MonoBehaviour).IsAssignableFrom(type) || | ||
typeof(Component).IsAssignableFrom(type), "Should be a valid Unity component"); | ||
} | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.