@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.
- package/README.md +1 -1
- package/bridge/com.ucp.bridge/Editor/AssemblyInfo.cs +3 -0
- package/bridge/com.ucp.bridge/Editor/AssemblyInfo.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Bridge/BridgeServer.cs +45 -5
- package/bridge/com.ucp.bridge/Editor/Compatibility/UnityObjectCompat.cs +26 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/AssetController.cs +2 -2
- package/bridge/com.ucp.bridge/Editor/Controllers/CompilationController.cs +137 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/EditorModalGuard.cs +60 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/EditorModalGuard.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/HierarchyController.cs +56 -5
- package/bridge/com.ucp.bridge/Editor/Controllers/MaterialController.cs +2 -2
- package/bridge/com.ucp.bridge/Editor/Controllers/ObjectLocator.cs +207 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/ObjectLocator.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/ObjectReferenceResolver.cs +1 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/PlayModeController.cs +1 -35
- package/bridge/com.ucp.bridge/Editor/Controllers/PrefabController.cs +3 -3
- package/bridge/com.ucp.bridge/Editor/Controllers/ProfilerController.cs +80 -5
- package/bridge/com.ucp.bridge/Editor/Controllers/PropertyController.cs +1 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/ReferenceController.cs +1 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/SceneChangeTracker.cs +6 -6
- package/bridge/com.ucp.bridge/Editor/Controllers/SceneController.cs +26 -36
- package/bridge/com.ucp.bridge/Editor/Controllers/ScriptController.cs +82 -23
- package/bridge/com.ucp.bridge/Editor/Controllers/SnapshotController.cs +4 -4
- package/bridge/com.ucp.bridge/Editor/Controllers/SpatialController.cs +322 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/SpatialController.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/TransformController.cs +249 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/TransformController.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/ViewController.cs +415 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/ViewController.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Protocol/MiniJson.cs +409 -63
- package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +131 -13
- package/bridge/com.ucp.bridge/Tests/Editor/MiniJsonSerializerTests.cs +221 -0
- package/bridge/com.ucp.bridge/Tests/Editor/MiniJsonSerializerTests.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Tests/Editor/SpatialVisualControllerTests.cs +252 -0
- package/bridge/com.ucp.bridge/Tests/Editor/SpatialVisualControllerTests.cs.meta +2 -0
- package/bridge/com.ucp.bridge/package.json +1 -1
- 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
|
-
|
|
311
|
-
|
|
312
|
-
Assert
|
|
313
|
-
|
|
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 =
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|