@mflrevan/ucp 0.5.2 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +1 -1
  2. package/bridge/com.ucp.bridge/Editor/AssemblyInfo.cs +3 -0
  3. package/bridge/com.ucp.bridge/Editor/AssemblyInfo.cs.meta +2 -0
  4. package/bridge/com.ucp.bridge/Editor/Bridge/BridgeServer.cs +45 -5
  5. package/bridge/com.ucp.bridge/Editor/Compatibility/UnityObjectCompat.cs +26 -0
  6. package/bridge/com.ucp.bridge/Editor/Controllers/AssetController.cs +2 -2
  7. package/bridge/com.ucp.bridge/Editor/Controllers/CompilationController.cs +137 -1
  8. package/bridge/com.ucp.bridge/Editor/Controllers/EditorModalGuard.cs +60 -0
  9. package/bridge/com.ucp.bridge/Editor/Controllers/EditorModalGuard.cs.meta +2 -0
  10. package/bridge/com.ucp.bridge/Editor/Controllers/HierarchyController.cs +56 -5
  11. package/bridge/com.ucp.bridge/Editor/Controllers/MaterialController.cs +2 -2
  12. package/bridge/com.ucp.bridge/Editor/Controllers/ObjectLocator.cs +207 -0
  13. package/bridge/com.ucp.bridge/Editor/Controllers/ObjectLocator.cs.meta +2 -0
  14. package/bridge/com.ucp.bridge/Editor/Controllers/ObjectReferenceResolver.cs +1 -1
  15. package/bridge/com.ucp.bridge/Editor/Controllers/PlayModeController.cs +1 -35
  16. package/bridge/com.ucp.bridge/Editor/Controllers/PrefabController.cs +3 -3
  17. package/bridge/com.ucp.bridge/Editor/Controllers/ProfilerController.cs +80 -5
  18. package/bridge/com.ucp.bridge/Editor/Controllers/PropertyController.cs +1 -1
  19. package/bridge/com.ucp.bridge/Editor/Controllers/ReferenceController.cs +1 -1
  20. package/bridge/com.ucp.bridge/Editor/Controllers/SceneChangeTracker.cs +6 -6
  21. package/bridge/com.ucp.bridge/Editor/Controllers/SceneController.cs +26 -36
  22. package/bridge/com.ucp.bridge/Editor/Controllers/ScriptController.cs +82 -23
  23. package/bridge/com.ucp.bridge/Editor/Controllers/SnapshotController.cs +4 -4
  24. package/bridge/com.ucp.bridge/Editor/Controllers/SpatialController.cs +322 -0
  25. package/bridge/com.ucp.bridge/Editor/Controllers/SpatialController.cs.meta +2 -0
  26. package/bridge/com.ucp.bridge/Editor/Controllers/TransformController.cs +249 -0
  27. package/bridge/com.ucp.bridge/Editor/Controllers/TransformController.cs.meta +2 -0
  28. package/bridge/com.ucp.bridge/Editor/Controllers/ViewController.cs +415 -0
  29. package/bridge/com.ucp.bridge/Editor/Controllers/ViewController.cs.meta +2 -0
  30. package/bridge/com.ucp.bridge/Editor/Protocol/MiniJson.cs +409 -63
  31. package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +131 -13
  32. package/bridge/com.ucp.bridge/Tests/Editor/MiniJsonSerializerTests.cs +221 -0
  33. package/bridge/com.ucp.bridge/Tests/Editor/MiniJsonSerializerTests.cs.meta +2 -0
  34. package/bridge/com.ucp.bridge/Tests/Editor/SpatialVisualControllerTests.cs +252 -0
  35. package/bridge/com.ucp.bridge/Tests/Editor/SpatialVisualControllerTests.cs.meta +2 -0
  36. package/bridge/com.ucp.bridge/package.json +1 -1
  37. package/package.json +1 -1
@@ -307,10 +307,48 @@ namespace UCP.Bridge.Tests
307
307
  var status = _router.Dispatch("logs/status", 1, "{}");
308
308
  Assert.That(status.error, Is.Null);
309
309
 
310
- var result = (Dictionary<string, object>)status.result;
311
- var byLevel = (Dictionary<string, object>)result["byLevel"];
312
- Assert.That(Convert.ToInt32(byLevel["error"]), Is.EqualTo(0));
313
- Assert.That(Convert.ToInt32(byLevel["exception"]), Is.EqualTo(0));
310
+ Assert.That(status.result, Is.InstanceOf<Dictionary<string, object>>());
311
+
312
+ // Assert on the specific regression this test exists for -- `asset/search` loading
313
+ // `.unity` files through LoadAllAssetsAtPath, which makes Unity emit
314
+ // "Do not use ReadObjectThreaded on scene objects!" -- rather than on "zero errors of
315
+ // any kind". A blanket count also catches unrelated editor noise: on a cold Library the
316
+ // search pulls in lazy imports whose URP shader-fallback errors have nothing to do with
317
+ // this code path, which made the whole release matrix red on every Unity version.
318
+ var problems = BufferedProblems();
319
+ var threadedReadErrors = problems
320
+ .FindAll(entry => entry.Contains("ReadObjectThreaded"));
321
+
322
+ Assert.That(
323
+ threadedReadErrors,
324
+ Is.Empty,
325
+ () => "asset/search emitted threaded scene-read errors:" + NewLineIndent
326
+ + string.Join(NewLineIndent, threadedReadErrors));
327
+ }
328
+
329
+ private const string NewLineIndent = "\n ";
330
+
331
+ /// Buffered error/exception entries, as "[level] message" strings.
332
+ private List<string> BufferedProblems()
333
+ {
334
+ var lines = new List<string>();
335
+ var tail = _router.Dispatch("logs/tail", 1, "{\"count\":200}");
336
+ if (tail.error != null || tail.result is not Dictionary<string, object> payload)
337
+ return lines;
338
+
339
+ if (!payload.TryGetValue("logs", out var logsObj) || logsObj is not List<object> logs)
340
+ return lines;
341
+
342
+ foreach (var item in logs)
343
+ {
344
+ if (item is not Dictionary<string, object> entry) continue;
345
+ var level = entry.TryGetValue("level", out var l) ? l?.ToString() : null;
346
+ if (level != "error" && level != "exception") continue;
347
+ var message = entry.TryGetValue("messagePreview", out var m) ? m?.ToString() : "";
348
+ lines.Add($"[{level}] {message}");
349
+ }
350
+
351
+ return lines;
314
352
  }
315
353
 
316
354
  [Test]
@@ -544,7 +582,7 @@ namespace UCP.Bridge.Tests
544
582
  );
545
583
  Assert.That(getPosition.error, Is.Null);
546
584
 
547
- var updated = EditorUtility.InstanceIDToObject(instanceId) as GameObject;
585
+ var updated = UnityObjectCompat.ResolveByInstanceId(instanceId) as GameObject;
548
586
  Assert.That(updated, Is.Not.Null);
549
587
  var localPosition = updated.transform.localPosition;
550
588
  Assert.That(localPosition.x, Is.EqualTo(1f).Within(0.001f));
@@ -556,7 +594,7 @@ namespace UCP.Bridge.Tests
556
594
 
557
595
  var delete = _router.Dispatch("object/delete", 1, "{\"instanceId\":" + instanceId + "}");
558
596
  Assert.That(delete.error, Is.Null);
559
- Assert.That(EditorUtility.InstanceIDToObject(instanceId), Is.Null);
597
+ Assert.That(UnityObjectCompat.ResolveByInstanceId(instanceId), Is.Null);
560
598
  }
561
599
 
562
600
  [Test]
@@ -573,7 +611,7 @@ namespace UCP.Bridge.Tests
573
611
  var response = _router.Dispatch(
574
612
  "object/set-property",
575
613
  1,
576
- "{\"instanceId\":" + go.GetInstanceID() + ",\"component\":\"ReferenceComponent\",\"property\":\"referenceAsset\",\"value\":{\"path\":\"" + TempReferenceAssetPath + "\"}}"
614
+ "{\"instanceId\":" + go.GetId() + ",\"component\":\"ReferenceComponent\",\"property\":\"referenceAsset\",\"value\":{\"path\":\"" + TempReferenceAssetPath + "\"}}"
577
615
  );
578
616
 
579
617
  Assert.That(response.error, Is.Null);
@@ -598,7 +636,7 @@ namespace UCP.Bridge.Tests
598
636
  var response = _router.Dispatch(
599
637
  "object/set-property",
600
638
  1,
601
- "{\"instanceId\":" + cube.GetInstanceID() + ",\"component\":\"MeshRenderer\",\"property\":\"m_Materials\",\"value\":[{\"path\":\"" + TempMaterialPath + "\"}]}"
639
+ "{\"instanceId\":" + cube.GetId() + ",\"component\":\"MeshRenderer\",\"property\":\"m_Materials\",\"value\":[{\"path\":\"" + TempMaterialPath + "\"}]}"
602
640
  );
603
641
 
604
642
  Assert.That(response.error, Is.Null);
@@ -616,7 +654,7 @@ namespace UCP.Bridge.Tests
616
654
  var response = _router.Dispatch(
617
655
  "object/set-property",
618
656
  1,
619
- "{\"instanceId\":" + go.GetInstanceID() + ",\"component\":\"ReferenceComponent\",\"property\":\"referenceAsset\",\"value\":{\"path\":\"Assets/Missing.asset\"}}"
657
+ "{\"instanceId\":" + go.GetId() + ",\"component\":\"ReferenceComponent\",\"property\":\"referenceAsset\",\"value\":{\"path\":\"Assets/Missing.asset\"}}"
620
658
  );
621
659
 
622
660
  Assert.That(response.error, Is.Not.Null);
@@ -990,6 +1028,62 @@ namespace UCP.Bridge.Tests
990
1028
  Assert.That(UnityEngine.SceneManagement.SceneManager.GetSceneByPath(TempSceneBPath).isLoaded, Is.True);
991
1029
  }
992
1030
 
1031
+ [Test]
1032
+ public void ModalGuard_AutoSavesDirtyTitledScene_WithoutPrompting()
1033
+ {
1034
+ var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
1035
+ Assert.That(EditorSceneManager.SaveScene(scene, TempScenePath), Is.True);
1036
+ new GameObject("ModalGuardDirtyMaker");
1037
+ EditorSceneManager.MarkSceneDirty(scene);
1038
+ Assert.That(scene.isDirty, Is.True);
1039
+
1040
+ EditorModalGuard.SaveOpenDirtyScenes(true, true);
1041
+
1042
+ Assert.That(scene.isDirty, Is.False);
1043
+ Assert.That(string.IsNullOrEmpty(scene.path), Is.False);
1044
+ }
1045
+
1046
+ [Test]
1047
+ public void ModalGuard_DiscardsDirtyUntitledScene_WhenDiscardAllowed()
1048
+ {
1049
+ var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
1050
+ new GameObject("ModalGuardUntitledDirtyMaker");
1051
+ EditorSceneManager.MarkSceneDirty(scene);
1052
+ Assert.That(string.IsNullOrEmpty(scene.path), Is.True);
1053
+ Assert.That(scene.isDirty, Is.True);
1054
+
1055
+ EditorModalGuard.SaveOpenDirtyScenes(true, true);
1056
+
1057
+ var active = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
1058
+ Assert.That(string.IsNullOrEmpty(active.path), Is.True);
1059
+ Assert.That(active.isDirty, Is.False);
1060
+ Assert.That(GameObject.Find("ModalGuardUntitledDirtyMaker"), Is.Null);
1061
+ }
1062
+
1063
+ [Test]
1064
+ public void ModalGuard_ThrowsOnDirtyUntitledScene_WhenDiscardDisallowed()
1065
+ {
1066
+ var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
1067
+ new GameObject("ModalGuardUntitledDirtyMaker");
1068
+ EditorSceneManager.MarkSceneDirty(scene);
1069
+
1070
+ Assert.That(
1071
+ () => EditorModalGuard.SaveOpenDirtyScenes(true, false),
1072
+ Throws.TypeOf<System.InvalidOperationException>());
1073
+ }
1074
+
1075
+ [Test]
1076
+ public void ModalGuard_LeavesSceneUntouched_WhenSavingDisabled()
1077
+ {
1078
+ var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
1079
+ new GameObject("ModalGuardUntitledDirtyMaker");
1080
+ EditorSceneManager.MarkSceneDirty(scene);
1081
+ Assert.That(scene.isDirty, Is.True);
1082
+
1083
+ Assert.That(() => EditorModalGuard.SaveOpenDirtyScenes(false, false), Throws.Nothing);
1084
+ Assert.That(UnityEngine.SceneManagement.SceneManager.GetActiveScene().isDirty, Is.True);
1085
+ }
1086
+
993
1087
  [Test]
994
1088
  public void PackagesController_DependencySetInfoAndRemove_LocalFilePackage()
995
1089
  {
@@ -1071,7 +1165,7 @@ namespace UCP.Bridge.Tests
1071
1165
  var response = _router.Dispatch(
1072
1166
  "scene/focus",
1073
1167
  1,
1074
- "{\"instanceId\":" + cube.GetInstanceID() + ",\"axis\":[1,0,1]}"
1168
+ "{\"instanceId\":" + cube.GetId() + ",\"axis\":[1,0,1]}"
1075
1169
  );
1076
1170
 
1077
1171
  Assert.That(response.error, Is.Null);
@@ -1086,9 +1180,33 @@ namespace UCP.Bridge.Tests
1086
1180
  System.Convert.ToSingle(axisData[0]),
1087
1181
  System.Convert.ToSingle(axisData[1]),
1088
1182
  System.Convert.ToSingle(axisData[2]));
1089
- var actualForward = sceneView.camera.transform.forward;
1090
1183
  Assert.That(Vector3.Dot(returnedAxis.normalized, expectedDirection), Is.GreaterThan(0.98f));
1091
- Assert.That(Mathf.Abs(Vector3.Dot(actualForward.normalized, expectedDirection)), Is.GreaterThan(0.98f));
1184
+
1185
+ // Assert against the Scene view's own state and the reported pose, not
1186
+ // `sceneView.camera.transform`: that transform is only synced when the view repaints,
1187
+ // so in batch mode it still holds the pre-focus pose and this check used to fail.
1188
+ var viewForward = sceneView.rotation * Vector3.forward;
1189
+ Assert.That(Mathf.Abs(Vector3.Dot(viewForward.normalized, expectedDirection)), Is.GreaterThan(0.98f));
1190
+
1191
+ var eulerData = (List<object>)result["cameraRotationEuler"];
1192
+ var reportedForward = Quaternion.Euler(
1193
+ System.Convert.ToSingle(eulerData[0]),
1194
+ System.Convert.ToSingle(eulerData[1]),
1195
+ System.Convert.ToSingle(eulerData[2])) * Vector3.forward;
1196
+ Assert.That(
1197
+ Mathf.Abs(Vector3.Dot(reportedForward.normalized, expectedDirection)),
1198
+ Is.GreaterThan(0.98f),
1199
+ "scene/focus must report the pose it just applied, not the last rendered one");
1200
+
1201
+ // The reported camera must sit behind the pivot along its own forward axis.
1202
+ var positionData = (List<object>)result["cameraPosition"];
1203
+ var reportedPosition = new Vector3(
1204
+ System.Convert.ToSingle(positionData[0]),
1205
+ System.Convert.ToSingle(positionData[1]),
1206
+ System.Convert.ToSingle(positionData[2]));
1207
+ Assert.That(Vector3.Dot((sceneView.pivot - reportedPosition).normalized, reportedForward.normalized),
1208
+ Is.GreaterThan(0.98f));
1209
+
1092
1210
  Assert.That(Vector3.Distance(sceneView.pivot, cube.transform.position), Is.LessThan(2f));
1093
1211
  }
1094
1212
 
@@ -1102,7 +1220,7 @@ namespace UCP.Bridge.Tests
1102
1220
  var response = _router.Dispatch(
1103
1221
  "scene/focus",
1104
1222
  1,
1105
- "{\"instanceId\":" + cube.GetInstanceID() + ",\"axis\":[0,0,0]}"
1223
+ "{\"instanceId\":" + cube.GetId() + ",\"axis\":[0,0,0]}"
1106
1224
  );
1107
1225
 
1108
1226
  Assert.That(response.error, Is.Not.Null);
@@ -0,0 +1,221 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using NUnit.Framework;
4
+ using UnityEngine;
5
+
6
+ namespace UCP.Bridge.Tests
7
+ {
8
+ /// <summary>
9
+ /// Regression coverage for the serializer that turns arbitrary <see cref="IUCPScript"/> return
10
+ /// values into JSON. The reflection walk used to be unbounded, so returning anything holding a
11
+ /// UnityEngine math struct (Vector3.normalized returns another Vector3, forever) overflowed the
12
+ /// stack -- which .NET cannot catch and which killed the editor process outright.
13
+ /// </summary>
14
+ public class MiniJsonSerializerTests
15
+ {
16
+ private static Dictionary<string, object> Roundtrip(object value)
17
+ {
18
+ var json = MiniJson.Serialize(new { value });
19
+ var parsed = MiniJson.Deserialize(json) as Dictionary<string, object>;
20
+ Assert.IsNotNull(parsed, $"Serializer produced unparseable JSON: {json}");
21
+ return parsed;
22
+ }
23
+
24
+ [Test]
25
+ public void SerializesVector3WithoutStackOverflow()
26
+ {
27
+ // The original crash repro: `return new { pos = Vector3.zero }` from a UCP script.
28
+ var json = MiniJson.Serialize(new { pos = Vector3.zero });
29
+
30
+ Assert.AreEqual("{\"pos\":{\"x\":0,\"y\":0,\"z\":0}}", json);
31
+ }
32
+
33
+ [Test]
34
+ public void SerializesUnityMathStructsAsPlainShapes()
35
+ {
36
+ Assert.AreEqual("{\"x\":1,\"y\":2}", MiniJson.Serialize(new Vector2(1f, 2f)));
37
+ Assert.AreEqual("{\"x\":1,\"y\":2,\"z\":3,\"w\":4}", MiniJson.Serialize(new Vector4(1f, 2f, 3f, 4f)));
38
+ Assert.AreEqual("{\"x\":0,\"y\":0,\"z\":0,\"w\":1}", MiniJson.Serialize(Quaternion.identity));
39
+ Assert.AreEqual("{\"r\":1,\"g\":0,\"b\":0,\"a\":1}", MiniJson.Serialize(Color.red));
40
+ Assert.AreEqual("{\"x\":1,\"y\":2,\"z\":3}", MiniJson.Serialize(new Vector3Int(1, 2, 3)));
41
+ Assert.AreEqual(
42
+ "{\"x\":1,\"y\":2,\"width\":3,\"height\":4}",
43
+ MiniJson.Serialize(new Rect(1f, 2f, 3f, 4f)));
44
+ Assert.AreEqual(
45
+ "{\"center\":{\"x\":0,\"y\":0,\"z\":0},\"size\":{\"x\":2,\"y\":2,\"z\":2}}",
46
+ MiniJson.Serialize(new Bounds(Vector3.zero, Vector3.one * 2f)));
47
+ }
48
+
49
+ [Test]
50
+ public void SerializesQuaternionNestedInAnonymousResult()
51
+ {
52
+ var parsed = Roundtrip(new { rot = Quaternion.Euler(0f, 90f, 0f), pos = Vector3.one });
53
+ Assert.IsInstanceOf<Dictionary<string, object>>(parsed["value"]);
54
+ }
55
+
56
+ [Test]
57
+ public void SerializesUnityObjectAsIdentityInsteadOfWalkingTheSceneGraph()
58
+ {
59
+ var go = new GameObject("MiniJsonProbe");
60
+ try
61
+ {
62
+ // GameObject.transform.gameObject is a cycle; walking it never terminates.
63
+ var parsed = Roundtrip(go);
64
+ var identity = (Dictionary<string, object>)parsed["value"];
65
+
66
+ Assert.AreEqual("MiniJsonProbe", identity["name"]);
67
+ Assert.AreEqual("GameObject", identity["type"]);
68
+ Assert.IsTrue(identity.ContainsKey("instanceId"));
69
+
70
+ // Components are the same story via Component.gameObject.
71
+ var componentJson = MiniJson.Serialize(go.transform);
72
+ Assert.IsTrue(componentJson.Contains("\"type\":\"Transform\""), componentJson);
73
+ }
74
+ finally
75
+ {
76
+ UnityEngine.Object.DestroyImmediate(go);
77
+ }
78
+ }
79
+
80
+ [Test]
81
+ public void DestroyedUnityObjectSerializesAsNull()
82
+ {
83
+ var go = new GameObject("MiniJsonDestroyed");
84
+ UnityEngine.Object.DestroyImmediate(go);
85
+
86
+ Assert.AreEqual("{\"value\":null}", MiniJson.Serialize(new { value = go }));
87
+ }
88
+
89
+ [Test]
90
+ public void ReferenceCyclesAreBrokenInsteadOfRecursingForever()
91
+ {
92
+ var a = new Node { Label = "a" };
93
+ var b = new Node { Label = "b", Next = a };
94
+ a.Next = b;
95
+
96
+ var json = MiniJson.Serialize(a);
97
+
98
+ Assert.IsTrue(json.Contains("<ucp:cycle>"), json);
99
+ Assert.IsNotNull(MiniJson.Deserialize(json));
100
+ }
101
+
102
+ [Test]
103
+ public void SelfReferencingDictionaryIsBroken()
104
+ {
105
+ var dict = new Dictionary<string, object> { ["name"] = "root" };
106
+ dict["self"] = dict;
107
+
108
+ var json = MiniJson.Serialize(dict);
109
+
110
+ Assert.IsTrue(json.Contains("<ucp:cycle>"), json);
111
+ Assert.IsNotNull(MiniJson.Deserialize(json));
112
+ }
113
+
114
+ [Test]
115
+ public void UnboundedComputedPropertyRecursionIsDepthCapped()
116
+ {
117
+ // Mirrors the Vector3.normalized shape for a type the serializer has no special case
118
+ // for: every read allocates a fresh instance, so reference tracking cannot help and
119
+ // only the depth cap prevents a stack overflow.
120
+ var json = MiniJson.Serialize(new Fractal());
121
+
122
+ Assert.IsTrue(json.Contains("<ucp:max-depth>"), json);
123
+ Assert.IsNotNull(MiniJson.Deserialize(json));
124
+ }
125
+
126
+ [Test]
127
+ public void ThrowingGetterDoesNotCorruptTheDocument()
128
+ {
129
+ var json = MiniJson.Serialize(new Explosive());
130
+
131
+ Assert.IsFalse(json.Contains("boom"), json);
132
+ var parsed = MiniJson.Deserialize(json) as Dictionary<string, object>;
133
+ Assert.IsNotNull(parsed, json);
134
+ Assert.AreEqual("ok", parsed["safe"]);
135
+ }
136
+
137
+ [Test]
138
+ public void NonFiniteFloatsSerializeAsNullRatherThanInvalidJson()
139
+ {
140
+ // Degenerate bounds and zero-length normalize hand out NaN routinely; raw NaN/Infinity
141
+ // are not valid JSON and made the whole response unparseable on the CLI side.
142
+ var json = MiniJson.Serialize(new { a = float.NaN, b = float.PositiveInfinity, c = double.NaN });
143
+
144
+ Assert.AreEqual("{\"a\":null,\"b\":null,\"c\":null}", json);
145
+ Assert.IsNotNull(MiniJson.Deserialize(json));
146
+ }
147
+
148
+ [Test]
149
+ public void UnsignedAndWideIntegersSerializeAsNumbers()
150
+ {
151
+ var json = MiniJson.Serialize(new { a = (uint)7, b = (ushort)8, c = (byte)9, d = 10UL });
152
+
153
+ Assert.AreEqual("{\"a\":7,\"b\":8,\"c\":9,\"d\":10}", json);
154
+ }
155
+
156
+ [Test]
157
+ public void NonListEnumerablesSerializeAsArrays()
158
+ {
159
+ var json = MiniJson.Serialize(new { items = new HashSet<int> { 1 } });
160
+
161
+ Assert.AreEqual("{\"items\":[1]}", json);
162
+ }
163
+
164
+ [Test]
165
+ public void EnumsStillSerializeAsIntegers()
166
+ {
167
+ Assert.AreEqual("{\"value\":2}", MiniJson.Serialize(new { value = SampleEnum.Two }));
168
+ }
169
+
170
+ [Test]
171
+ public void ParserRejectsTruncatedInputInsteadOfHanging()
172
+ {
173
+ // A truncated string literal used to spin the reader on end-of-input forever, wedging
174
+ // the editor's main thread.
175
+ Assert.Throws<FormatException>(() => MiniJson.Deserialize("{\"a\": \"unterminated"));
176
+ }
177
+
178
+ [Test]
179
+ public void ParserRejectsPathologicallyNestedInput()
180
+ {
181
+ var deep = new string('[', 1000);
182
+ Assert.Throws<FormatException>(() => MiniJson.Deserialize(deep));
183
+ }
184
+
185
+ [Test]
186
+ public void OrdinaryPayloadsAreUnchanged()
187
+ {
188
+ var json = MiniJson.Serialize(new Dictionary<string, object>
189
+ {
190
+ ["name"] = "cube",
191
+ ["active"] = true,
192
+ ["children"] = new List<object> { 1L, 2.5, null },
193
+ });
194
+
195
+ Assert.AreEqual("{\"name\":\"cube\",\"active\":true,\"children\":[1,2.5,null]}", json);
196
+ }
197
+
198
+ private enum SampleEnum
199
+ {
200
+ One = 1,
201
+ Two = 2,
202
+ }
203
+
204
+ private sealed class Node
205
+ {
206
+ public string Label;
207
+ public Node Next;
208
+ }
209
+
210
+ private sealed class Fractal
211
+ {
212
+ public Fractal Child => new Fractal();
213
+ }
214
+
215
+ private sealed class Explosive
216
+ {
217
+ public string Safe => "ok";
218
+ public string Boom => throw new InvalidOperationException("boom");
219
+ }
220
+ }
221
+ }
@@ -0,0 +1,2 @@
1
+ fileFormatVersion: 2
2
+ guid: 5082955dbcce78c4986000348e3a5d92