@mflrevan/ucp 0.6.0 → 0.6.2
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/CHANGELOG.md +62 -0
- package/bridge/com.ucp.bridge/Editor/Bridge/BridgeServer.cs +42 -5
- package/bridge/com.ucp.bridge/Editor/Controllers/CompilationController.cs +137 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/ProfilerController.cs +80 -5
- package/bridge/com.ucp.bridge/Editor/Controllers/PropertyController.cs +23 -7
- package/bridge/com.ucp.bridge/Editor/Controllers/RecordingController.cs +724 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/RecordingController.cs.meta +11 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/SceneController.cs +24 -2
- package/bridge/com.ucp.bridge/Editor/Controllers/ScriptController.cs +82 -23
- package/bridge/com.ucp.bridge/Editor/Controllers/TestRunnerController.cs +10 -1
- package/bridge/com.ucp.bridge/Editor/Protocol/MiniJson.cs +409 -63
- package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +181 -6
- 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/package.json +1 -1
- package/package.json +1 -1
|
@@ -46,6 +46,7 @@ namespace UCP.Bridge.Tests
|
|
|
46
46
|
PlayModeController.Register(_router);
|
|
47
47
|
ReferenceController.Register(_router);
|
|
48
48
|
LogsController.Register(_router);
|
|
49
|
+
RecordingController.Register(_router);
|
|
49
50
|
HierarchyController.Register(_router);
|
|
50
51
|
ProfilerController.Register(_router);
|
|
51
52
|
PropertyController.Register(_router);
|
|
@@ -72,6 +73,7 @@ namespace UCP.Bridge.Tests
|
|
|
72
73
|
DeleteTempLocalPackage();
|
|
73
74
|
RemoveTempLocalPackageDependencyIfPresent();
|
|
74
75
|
LogsController.ClearHistoryForTests();
|
|
76
|
+
RecordingController.ResetForTests();
|
|
75
77
|
AssetImportSupport.ClearTestState();
|
|
76
78
|
Profiler.enabled = false;
|
|
77
79
|
Profiler.enableBinaryLog = false;
|
|
@@ -97,6 +99,7 @@ namespace UCP.Bridge.Tests
|
|
|
97
99
|
DeleteTempLocalPackage();
|
|
98
100
|
RemoveTempLocalPackageDependencyIfPresent();
|
|
99
101
|
LogsController.ClearHistoryForTests();
|
|
102
|
+
RecordingController.ResetForTests();
|
|
100
103
|
AssetImportSupport.ClearTestState();
|
|
101
104
|
Profiler.enabled = false;
|
|
102
105
|
Profiler.enableBinaryLog = false;
|
|
@@ -123,6 +126,115 @@ namespace UCP.Bridge.Tests
|
|
|
123
126
|
Assert.That(Convert.ToBoolean(capabilities["sessionControl"]), Is.True);
|
|
124
127
|
}
|
|
125
128
|
|
|
129
|
+
[Test]
|
|
130
|
+
public void RecordingController_RegistersLifecycleMethodsAndReportsIdle()
|
|
131
|
+
{
|
|
132
|
+
Assert.That(_router.HasMethod("record/start"), Is.True);
|
|
133
|
+
Assert.That(_router.HasMethod("record/stop"), Is.True);
|
|
134
|
+
Assert.That(_router.HasMethod("record/status"), Is.True);
|
|
135
|
+
Assert.That(_router.HasMethod("record/arm"), Is.True);
|
|
136
|
+
Assert.That(_router.HasMethod("record/signal"), Is.True);
|
|
137
|
+
|
|
138
|
+
var response = _router.Dispatch("record/status", 1, "{}");
|
|
139
|
+
Assert.That(response.error, Is.Null);
|
|
140
|
+
var result = (Dictionary<string, object>)response.result;
|
|
141
|
+
Assert.That(result["state"], Is.EqualTo("idle"));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
[Test]
|
|
145
|
+
public void RecordingController_ResolvesEvenAspectPreservingDimensions()
|
|
146
|
+
{
|
|
147
|
+
Assert.That(RecordingController.ResolveDimensionsForTests(16f / 9f, 960, null, null),
|
|
148
|
+
Is.EqualTo((960, 540)));
|
|
149
|
+
Assert.That(RecordingController.ResolveDimensionsForTests(9f / 16f, 960, null, null),
|
|
150
|
+
Is.EqualTo((540, 960)));
|
|
151
|
+
Assert.That(RecordingController.ResolveDimensionsForTests(4f / 3f, 960, 640, null),
|
|
152
|
+
Is.EqualTo((640, 480)));
|
|
153
|
+
Assert.That(RecordingController.ResolveDimensionsForTests(4f / 3f, 960, null, 600),
|
|
154
|
+
Is.EqualTo((800, 600)));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
[Test]
|
|
158
|
+
public void RecordingController_ArmsAndMatchesNamedSignal()
|
|
159
|
+
{
|
|
160
|
+
var response = _router.Dispatch(
|
|
161
|
+
"record/arm",
|
|
162
|
+
1,
|
|
163
|
+
"{\"trigger\":\"signal:not-this-one\",\"duration\":1,\"timeout\":5}");
|
|
164
|
+
Assert.That(response.error, Is.Null);
|
|
165
|
+
|
|
166
|
+
var signal = _router.Dispatch("record/signal", 2, "{\"name\":\"different\"}");
|
|
167
|
+
Assert.That(signal.error, Is.Null);
|
|
168
|
+
var result = (Dictionary<string, object>)signal.result;
|
|
169
|
+
Assert.That(Convert.ToBoolean(result["matched"]), Is.False);
|
|
170
|
+
Assert.That(result["state"], Is.EqualTo("armed"));
|
|
171
|
+
|
|
172
|
+
var stop = _router.Dispatch("record/stop", 3, "{}");
|
|
173
|
+
Assert.That(stop.error, Is.Null);
|
|
174
|
+
var stopped = (Dictionary<string, object>)stop.result;
|
|
175
|
+
Assert.That(stopped["state"], Is.EqualTo("idle"));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
[Test]
|
|
179
|
+
public void GetProperty_ReturnsCompositeValuesRatherThanTypeNames()
|
|
180
|
+
{
|
|
181
|
+
// Regression: when SerializedObject.FindProperty resolves a name, the value it returns
|
|
182
|
+
// is already JSON-shaped. That result used to be converted a second time, match no
|
|
183
|
+
// case, and fall through to value.ToString() -- so a Vector3 arrived as
|
|
184
|
+
// "System.Collections.Generic.List`1[System.Object]". Querying the serialized name
|
|
185
|
+
// (m_LocalPosition) is what forces the FindProperty path; the public alias (position)
|
|
186
|
+
// has no serialized entry and takes the reflection fallback instead, which is why that
|
|
187
|
+
// one never looked broken.
|
|
188
|
+
var go = new GameObject("UcpPropertyProbe");
|
|
189
|
+
try
|
|
190
|
+
{
|
|
191
|
+
go.transform.localPosition = new Vector3(1.5f, 2.5f, 3.5f);
|
|
192
|
+
var id = go.GetInstanceID();
|
|
193
|
+
|
|
194
|
+
var serialized = _router.Dispatch("object/get-property", 1, "{\"instanceId\":ID,\"component\":\"Transform\",\"property\":\"m_LocalPosition\"}".Replace("ID", id.ToString()));
|
|
195
|
+
Assert.That(serialized.error, Is.Null);
|
|
196
|
+
var serializedResult = (Dictionary<string, object>)serialized.result;
|
|
197
|
+
var localPosition = serializedResult["value"] as IList;
|
|
198
|
+
Assert.That(localPosition, Is.Not.Null,
|
|
199
|
+
"a serialized Vector3 must come back as an array, not a stringified type name");
|
|
200
|
+
Assert.That(localPosition.Count, Is.EqualTo(3));
|
|
201
|
+
Assert.That(Convert.ToDouble(localPosition[0]), Is.EqualTo(1.5d).Within(1e-4));
|
|
202
|
+
Assert.That(Convert.ToDouble(localPosition[2]), Is.EqualTo(3.5d).Within(1e-4));
|
|
203
|
+
Assert.That(serializedResult["type"], Is.EqualTo("Vector3"),
|
|
204
|
+
"type must describe the field, not the container it was shaped into");
|
|
205
|
+
|
|
206
|
+
// Reflection fallback: no serialized entry named "position".
|
|
207
|
+
var reflected = _router.Dispatch("object/get-property", 2, "{\"instanceId\":ID,\"component\":\"Transform\",\"property\":\"position\"}".Replace("ID", id.ToString()));
|
|
208
|
+
Assert.That(reflected.error, Is.Null);
|
|
209
|
+
var reflectedResult = (Dictionary<string, object>)reflected.result;
|
|
210
|
+
Assert.That(reflectedResult["value"] as IList, Is.Not.Null);
|
|
211
|
+
Assert.That(reflectedResult["type"], Is.EqualTo("Vector3"));
|
|
212
|
+
}
|
|
213
|
+
finally
|
|
214
|
+
{
|
|
215
|
+
UnityEngine.Object.DestroyImmediate(go);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
[Test]
|
|
220
|
+
public void RecordingController_ValidatesSlowdownRange()
|
|
221
|
+
{
|
|
222
|
+
// --slowdown exists to raise effective temporal resolution for models that sample a
|
|
223
|
+
// clip at a fixed low rate, so an out-of-range factor must fail loudly rather than
|
|
224
|
+
// silently producing a file whose playback rate is meaningless.
|
|
225
|
+
var tooLarge = _router.Dispatch("record/arm", 1, "{\"trigger\":\"signal:sd\",\"duration\":1,\"timeout\":5,\"slowdown\":40}");
|
|
226
|
+
Assert.That(tooLarge.error, Is.Not.Null, "a slowdown above the supported range must be rejected");
|
|
227
|
+
|
|
228
|
+
var accepted = _router.Dispatch("record/arm", 2, "{\"trigger\":\"signal:sd\",\"duration\":1,\"timeout\":5,\"slowdown\":6}");
|
|
229
|
+
Assert.That(accepted.error, Is.Null);
|
|
230
|
+
var armed = (Dictionary<string, object>)accepted.result;
|
|
231
|
+
Assert.That(armed["state"], Is.EqualTo("armed"));
|
|
232
|
+
|
|
233
|
+
var stop = _router.Dispatch("record/stop", 3, "{}");
|
|
234
|
+
Assert.That(stop.error, Is.Null);
|
|
235
|
+
Assert.That(((Dictionary<string, object>)stop.result)["state"], Is.EqualTo("idle"));
|
|
236
|
+
}
|
|
237
|
+
|
|
126
238
|
[Test]
|
|
127
239
|
public void ProfilerSessionStartStop_TogglesProfilerState()
|
|
128
240
|
{
|
|
@@ -307,10 +419,48 @@ namespace UCP.Bridge.Tests
|
|
|
307
419
|
var status = _router.Dispatch("logs/status", 1, "{}");
|
|
308
420
|
Assert.That(status.error, Is.Null);
|
|
309
421
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
Assert
|
|
313
|
-
|
|
422
|
+
Assert.That(status.result, Is.InstanceOf<Dictionary<string, object>>());
|
|
423
|
+
|
|
424
|
+
// Assert on the specific regression this test exists for -- `asset/search` loading
|
|
425
|
+
// `.unity` files through LoadAllAssetsAtPath, which makes Unity emit
|
|
426
|
+
// "Do not use ReadObjectThreaded on scene objects!" -- rather than on "zero errors of
|
|
427
|
+
// any kind". A blanket count also catches unrelated editor noise: on a cold Library the
|
|
428
|
+
// search pulls in lazy imports whose URP shader-fallback errors have nothing to do with
|
|
429
|
+
// this code path, which made the whole release matrix red on every Unity version.
|
|
430
|
+
var problems = BufferedProblems();
|
|
431
|
+
var threadedReadErrors = problems
|
|
432
|
+
.FindAll(entry => entry.Contains("ReadObjectThreaded"));
|
|
433
|
+
|
|
434
|
+
Assert.That(
|
|
435
|
+
threadedReadErrors,
|
|
436
|
+
Is.Empty,
|
|
437
|
+
() => "asset/search emitted threaded scene-read errors:" + NewLineIndent
|
|
438
|
+
+ string.Join(NewLineIndent, threadedReadErrors));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private const string NewLineIndent = "\n ";
|
|
442
|
+
|
|
443
|
+
/// Buffered error/exception entries, as "[level] message" strings.
|
|
444
|
+
private List<string> BufferedProblems()
|
|
445
|
+
{
|
|
446
|
+
var lines = new List<string>();
|
|
447
|
+
var tail = _router.Dispatch("logs/tail", 1, "{\"count\":200}");
|
|
448
|
+
if (tail.error != null || tail.result is not Dictionary<string, object> payload)
|
|
449
|
+
return lines;
|
|
450
|
+
|
|
451
|
+
if (!payload.TryGetValue("logs", out var logsObj) || logsObj is not List<object> logs)
|
|
452
|
+
return lines;
|
|
453
|
+
|
|
454
|
+
foreach (var item in logs)
|
|
455
|
+
{
|
|
456
|
+
if (item is not Dictionary<string, object> entry) continue;
|
|
457
|
+
var level = entry.TryGetValue("level", out var l) ? l?.ToString() : null;
|
|
458
|
+
if (level != "error" && level != "exception") continue;
|
|
459
|
+
var message = entry.TryGetValue("messagePreview", out var m) ? m?.ToString() : "";
|
|
460
|
+
lines.Add($"[{level}] {message}");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return lines;
|
|
314
464
|
}
|
|
315
465
|
|
|
316
466
|
[Test]
|
|
@@ -1142,9 +1292,33 @@ namespace UCP.Bridge.Tests
|
|
|
1142
1292
|
System.Convert.ToSingle(axisData[0]),
|
|
1143
1293
|
System.Convert.ToSingle(axisData[1]),
|
|
1144
1294
|
System.Convert.ToSingle(axisData[2]));
|
|
1145
|
-
var actualForward = sceneView.camera.transform.forward;
|
|
1146
1295
|
Assert.That(Vector3.Dot(returnedAxis.normalized, expectedDirection), Is.GreaterThan(0.98f));
|
|
1147
|
-
|
|
1296
|
+
|
|
1297
|
+
// Assert against the Scene view's own state and the reported pose, not
|
|
1298
|
+
// `sceneView.camera.transform`: that transform is only synced when the view repaints,
|
|
1299
|
+
// so in batch mode it still holds the pre-focus pose and this check used to fail.
|
|
1300
|
+
var viewForward = sceneView.rotation * Vector3.forward;
|
|
1301
|
+
Assert.That(Mathf.Abs(Vector3.Dot(viewForward.normalized, expectedDirection)), Is.GreaterThan(0.98f));
|
|
1302
|
+
|
|
1303
|
+
var eulerData = (List<object>)result["cameraRotationEuler"];
|
|
1304
|
+
var reportedForward = Quaternion.Euler(
|
|
1305
|
+
System.Convert.ToSingle(eulerData[0]),
|
|
1306
|
+
System.Convert.ToSingle(eulerData[1]),
|
|
1307
|
+
System.Convert.ToSingle(eulerData[2])) * Vector3.forward;
|
|
1308
|
+
Assert.That(
|
|
1309
|
+
Mathf.Abs(Vector3.Dot(reportedForward.normalized, expectedDirection)),
|
|
1310
|
+
Is.GreaterThan(0.98f),
|
|
1311
|
+
"scene/focus must report the pose it just applied, not the last rendered one");
|
|
1312
|
+
|
|
1313
|
+
// The reported camera must sit behind the pivot along its own forward axis.
|
|
1314
|
+
var positionData = (List<object>)result["cameraPosition"];
|
|
1315
|
+
var reportedPosition = new Vector3(
|
|
1316
|
+
System.Convert.ToSingle(positionData[0]),
|
|
1317
|
+
System.Convert.ToSingle(positionData[1]),
|
|
1318
|
+
System.Convert.ToSingle(positionData[2]));
|
|
1319
|
+
Assert.That(Vector3.Dot((sceneView.pivot - reportedPosition).normalized, reportedForward.normalized),
|
|
1320
|
+
Is.GreaterThan(0.98f));
|
|
1321
|
+
|
|
1148
1322
|
Assert.That(Vector3.Distance(sceneView.pivot, cube.transform.position), Is.LessThan(2f));
|
|
1149
1323
|
}
|
|
1150
1324
|
|
|
@@ -1585,4 +1759,5 @@ namespace UCP.Bridge.Tests
|
|
|
1585
1759
|
public SearchRootAsset referenceAsset;
|
|
1586
1760
|
}
|
|
1587
1761
|
}
|
|
1762
|
+
|
|
1588
1763
|
}
|
|
@@ -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
|
+
}
|