Skip to content

Commit 784ecd4

Browse files
committed
Improve material ops logging and test coverage
1 parent 753e156 commit 784ecd4

File tree

5 files changed

+109
-34
lines changed

5 files changed

+109
-34
lines changed

MCPForUnity/Editor/Helpers/MaterialOps.cs

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,10 @@ JToken GetValue(string key)
102102
modified = true;
103103
}
104104
}
105-
catch { }
105+
catch (Exception ex)
106+
{
107+
Debug.LogWarning($"[MaterialOps] Failed to set float property '{propName}': {ex.Message}");
108+
}
106109
}
107110
}
108111

@@ -229,36 +232,57 @@ public static bool TrySetShaderProperty(Material material, string propertyName,
229232
if (material.HasProperty(propertyName))
230233
{
231234
try { material.SetColor(propertyName, ParseColor(value, serializer)); return true; }
232-
catch (Exception ex) { Debug.LogWarning($"[MaterialOps] SetColor failed: {ex.Message}"); }
233-
235+
catch (Exception ex)
236+
{
237+
// Log at Debug level since we'll try other conversions
238+
Debug.Log($"[MaterialOps] SetColor attempt for '{propertyName}' failed: {ex.Message}");
239+
}
240+
234241
try { Vector4 vec = value.ToObject<Vector4>(serializer); material.SetVector(propertyName, vec); return true; }
235-
catch (Exception ex) { Debug.LogWarning($"[MaterialOps] SetVector (Vec4) failed: {ex.Message}"); }
242+
catch (Exception ex)
243+
{
244+
Debug.Log($"[MaterialOps] SetVector (Vec4) attempt for '{propertyName}' failed: {ex.Message}");
245+
}
236246
}
237247
}
238248
else if (jArray.Count == 3)
239249
{
240250
if (material.HasProperty(propertyName))
241251
{
242252
try { material.SetColor(propertyName, ParseColor(value, serializer)); return true; }
243-
catch (Exception ex) { Debug.LogWarning($"[MaterialOps] SetColor (Vec3) failed: {ex.Message}"); }
253+
catch (Exception ex)
254+
{
255+
Debug.Log($"[MaterialOps] SetColor (Vec3) attempt for '{propertyName}' failed: {ex.Message}");
256+
}
244257
}
245258
}
246259
else if (jArray.Count == 2)
247260
{
248261
if (material.HasProperty(propertyName))
249262
{
250263
try { Vector2 vec = value.ToObject<Vector2>(serializer); material.SetVector(propertyName, vec); return true; }
251-
catch (Exception ex) { Debug.LogWarning($"[MaterialOps] SetVector (Vec2) failed: {ex.Message}"); }
264+
catch (Exception ex)
265+
{
266+
Debug.Log($"[MaterialOps] SetVector (Vec2) attempt for '{propertyName}' failed: {ex.Message}");
267+
}
252268
}
253269
}
254270
}
255271
else if (value.Type == JTokenType.Float || value.Type == JTokenType.Integer)
256272
{
257-
try { material.SetFloat(propertyName, value.ToObject<float>(serializer)); return true; } catch { }
273+
try { material.SetFloat(propertyName, value.ToObject<float>(serializer)); return true; }
274+
catch (Exception ex)
275+
{
276+
Debug.Log($"[MaterialOps] SetFloat attempt for '{propertyName}' failed: {ex.Message}");
277+
}
258278
}
259279
else if (value.Type == JTokenType.Boolean)
260280
{
261-
try { material.SetFloat(propertyName, value.ToObject<bool>(serializer) ? 1f : 0f); return true; } catch { }
281+
try { material.SetFloat(propertyName, value.ToObject<bool>(serializer) ? 1f : 0f); return true; }
282+
catch (Exception ex)
283+
{
284+
Debug.Log($"[MaterialOps] SetFloat (bool) attempt for '{propertyName}' failed: {ex.Message}");
285+
}
262286
}
263287
else if (value.Type == JTokenType.String)
264288
{
@@ -341,9 +365,21 @@ public static Color ParseColor(JToken token, JsonSerializer serializer)
341365
1f
342366
);
343367
}
368+
else
369+
{
370+
throw new ArgumentException("Color array must have 3 or 4 elements.");
371+
}
344372
}
345373

346-
return token.ToObject<Color>(serializer);
374+
try
375+
{
376+
return token.ToObject<Color>(serializer);
377+
}
378+
catch (Exception ex)
379+
{
380+
Debug.LogWarning($"[MaterialOps] Failed to parse color from token: {ex.Message}");
381+
throw;
382+
}
347383
}
348384
}
349385
}

MCPForUnity/Editor/Tools/ManageMaterial.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
namespace MCPForUnity.Editor.Tools
1010
{
1111
[McpForUnityTool("manage_material", AutoRegister = false)]
12-
public class ManageMaterial
12+
public static class ManageMaterial
1313
{
1414
public static object HandleCommand(JObject @params)
1515
{
@@ -280,7 +280,7 @@ private static object SetRendererColor(JObject @params)
280280
MaterialPropertyBlock block = new MaterialPropertyBlock();
281281
renderer.GetPropertyBlock(block, slot);
282282

283-
if (slot < renderer.sharedMaterials.Length && renderer.sharedMaterials[slot] != null)
283+
if (renderer.sharedMaterials[slot] != null)
284284
{
285285
Material mat = renderer.sharedMaterials[slot];
286286
if (mat.HasProperty("_BaseColor")) block.SetColor("_BaseColor", color);
@@ -301,6 +301,10 @@ private static object SetRendererColor(JObject @params)
301301
if (slot >= 0 && slot < renderer.sharedMaterials.Length)
302302
{
303303
Material mat = renderer.sharedMaterials[slot];
304+
if (mat == null)
305+
{
306+
return new { status = "error", message = $"No material in slot {slot}" };
307+
}
304308
Undo.RecordObject(mat, "Set Material Color");
305309
if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", color);
306310
else mat.SetColor("_Color", color);
@@ -314,6 +318,10 @@ private static object SetRendererColor(JObject @params)
314318
if (slot >= 0 && slot < renderer.materials.Length)
315319
{
316320
Material mat = renderer.materials[slot];
321+
if (mat == null)
322+
{
323+
return new { status = "error", message = $"No material in slot {slot}" };
324+
}
317325
// Note: Undo cannot fully revert material instantiation
318326
Undo.RecordObject(mat, "Set Instance Material Color");
319327
if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", color);

Server/src/services/tools/manage_material.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,10 @@ async def manage_material(
6161
try:
6262
slot = int(slot)
6363
except ValueError:
64-
pass # Let it fail downstream; C# expects int
64+
return {
65+
"success": False,
66+
"message": f"Invalid slot value: '{slot}' must be a valid integer"
67+
}
6568

6669
# Prepare parameters for the C# handler
6770
params_dict = {
@@ -82,6 +85,11 @@ async def manage_material(
8285
params_dict = {k: v for k, v in params_dict.items() if v is not None}
8386

8487
# Use centralized async retry helper with instance routing
85-
result = await send_with_unity_instance(async_send_command_with_retry, unity_instance, "manage_material", params_dict)
88+
result = await send_with_unity_instance(
89+
async_send_command_with_retry,
90+
unity_instance,
91+
"manage_material",
92+
params_dict,
93+
)
8694

8795
return result if isinstance(result, dict) else {"success": False, "message": str(result)}

Server/tests/integration/test_manage_asset_json_parsing.py

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ class TestManageGameObjectJsonParsing:
120120

121121
@pytest.mark.asyncio
122122
async def test_component_properties_json_string_parsing(self, monkeypatch):
123-
"""Test that JSON string component_properties are correctly parsed."""
123+
"""Test that JSON string component_properties result in successful operation."""
124124
from services.tools.manage_gameobject import manage_gameobject
125125

126126
ctx = DummyContext()
@@ -140,20 +140,9 @@ async def fake_send(cmd, params, **kwargs):
140140
component_properties='{"MeshRenderer": {"material": "Assets/Materials/BlueMaterial.mat"}}'
141141
)
142142

143-
# Verify JSON parsing was logged
144-
145143
# Verify the result
146144
assert result["success"] is True
147-
148-
# Verify that component_properties reached Unity as a dict
149-
# We can't easily check 'captured_params' here without refactoring the test to capture args,
150-
# but since we mocked the transport, we can trust the return value and rely on
151-
# unit tests for parse_json_payload.
152-
# However, to follow the feedback, let's verify implicit behavior or refactor.
153-
# Since I cannot easily monkeypatch a capture variable here without changing the test structure significantly,
154-
# I will rely on the fact that if it wasn't parsed, it would likely fail downstream or be passed as string.
155-
# The feedback suggested: "captured_params = {} ... monkeypatch ... assert isinstance(captured_params...)"
156-
# I'll implement that pattern.
145+
157146

158147
@pytest.mark.asyncio
159148
async def test_component_properties_parsing_verification(self, monkeypatch):

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,39 +15,73 @@ public class ReadConsoleTests
1515
public void HandleCommand_Clear_Works()
1616
{
1717
// Arrange
18-
var paramsObj = new JObject
19-
{
20-
["action"] = "clear"
21-
};
18+
// Ensure there's something to clear
19+
Debug.Log("Log to clear");
20+
21+
// Verify content exists before clear
22+
var getBefore = ToJObject(ReadConsole.HandleCommand(new JObject { ["action"] = "get", ["count"] = 10 }));
23+
var entriesBefore = getBefore["data"] as JArray;
24+
25+
// Ideally we'd assert count > 0, but other tests/system logs might affect this.
26+
// Just ensuring the call doesn't fail is a baseline, but let's try to be stricter if possible.
27+
// Since we just logged, there should be at least one entry.
28+
Assert.IsTrue(entriesBefore != null && entriesBefore.Count > 0, "Setup failed: console should have logs.");
2229

2330
// Act
24-
var result = ToJObject(ReadConsole.HandleCommand(paramsObj));
31+
var result = ToJObject(ReadConsole.HandleCommand(new JObject { ["action"] = "clear" }));
2532

2633
// Assert
2734
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
35+
36+
// Verify clear effect
37+
var getAfter = ToJObject(ReadConsole.HandleCommand(new JObject { ["action"] = "get", ["count"] = 10 }));
38+
var entriesAfter = getAfter["data"] as JArray;
39+
Assert.IsTrue(entriesAfter == null || entriesAfter.Count == 0, "Console should be empty after clear.");
2840
}
2941

3042
[Test]
3143
public void HandleCommand_Get_Works()
3244
{
3345
// Arrange
34-
Debug.Log("Test Log Message"); // Ensure there is at least one log
46+
string uniqueMessage = $"Test Log Message {Guid.NewGuid()}";
47+
Debug.Log(uniqueMessage);
48+
3549
var paramsObj = new JObject
3650
{
3751
["action"] = "get",
38-
["count"] = 5
52+
["count"] = 10 // Fetch enough to likely catch our message
3953
};
4054

4155
// Act
4256
var result = ToJObject(ReadConsole.HandleCommand(paramsObj));
4357

4458
// Assert
4559
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
46-
Assert.IsInstanceOf<JArray>(result["data"]);
60+
var data = result["data"] as JArray;
61+
Assert.IsNotNull(data, "Data array should not be null.");
62+
Assert.IsTrue(data.Count > 0, "Should retrieve at least one log entry.");
63+
64+
// Verify content
65+
bool found = false;
66+
foreach (var entry in data)
67+
{
68+
if (entry["message"]?.ToString().Contains(uniqueMessage) == true)
69+
{
70+
found = true;
71+
break;
72+
}
73+
}
74+
Assert.IsTrue(found, $"The unique log message '{uniqueMessage}' was not found in retrieved logs.");
4775
}
4876

4977
private static JObject ToJObject(object result)
5078
{
79+
if (result == null)
80+
{
81+
Assert.Fail("ReadConsole.HandleCommand returned null.");
82+
return new JObject(); // Unreachable, but satisfies return type.
83+
}
84+
5185
return result as JObject ?? JObject.FromObject(result);
5286
}
5387
}

0 commit comments

Comments
 (0)