@mflrevan/ucp 0.4.6 → 0.5.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/Bridge/BridgeServer.cs +4 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/AssetController.cs +553 -7
- package/bridge/com.ucp.bridge/Editor/Controllers/AssetImportSupport.cs +41 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/ImporterController.cs +4 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/LogsController.cs +103 -75
- package/bridge/com.ucp.bridge/Editor/Controllers/ReferenceController.cs +163 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/ReferenceController.cs.meta +11 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/SceneController.cs +9 -3
- package/bridge/com.ucp.bridge/Editor/Controllers/TestRunnerController.cs +80 -4
- package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +375 -0
- package/bridge/com.ucp.bridge/package.json +1 -1
- package/package.json +2 -2
|
@@ -80,6 +80,47 @@ namespace UCP.Bridge
|
|
|
80
80
|
return CreateReimportResult(requestedPath, assetPath, true, false, null);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
public static Dictionary<string, object> ReimportRecursive(string requestedPath, bool forceSynchronous = true)
|
|
84
|
+
{
|
|
85
|
+
var assetPath = GetPrimaryAssetPath(requestedPath);
|
|
86
|
+
if (string.IsNullOrEmpty(assetPath))
|
|
87
|
+
throw new ArgumentException("Missing 'path' parameter");
|
|
88
|
+
if (!IsProjectAssetPath(assetPath))
|
|
89
|
+
throw new ArgumentException($"Path is not under Assets/ or Packages/: {requestedPath}");
|
|
90
|
+
|
|
91
|
+
if (!AssetDatabase.IsValidFolder(assetPath))
|
|
92
|
+
return Reimport(requestedPath, forceSynchronous);
|
|
93
|
+
|
|
94
|
+
var options = ImportAssetOptions.ForceUpdate;
|
|
95
|
+
if (forceSynchronous)
|
|
96
|
+
options |= ImportAssetOptions.ForceSynchronousImport;
|
|
97
|
+
|
|
98
|
+
var guidResults = AssetDatabase.FindAssets(string.Empty, new[] { assetPath });
|
|
99
|
+
var importedPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
100
|
+
|
|
101
|
+
foreach (var guid in guidResults)
|
|
102
|
+
{
|
|
103
|
+
var childPath = AssetDatabase.GUIDToAssetPath(guid);
|
|
104
|
+
if (string.IsNullOrEmpty(childPath) || AssetDatabase.IsValidFolder(childPath))
|
|
105
|
+
continue;
|
|
106
|
+
|
|
107
|
+
AssetDatabase.ImportAsset(childPath, options);
|
|
108
|
+
importedPaths.Add(childPath);
|
|
109
|
+
RecordReimport(childPath);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return new Dictionary<string, object>
|
|
113
|
+
{
|
|
114
|
+
["requestedPath"] = requestedPath,
|
|
115
|
+
["assetPath"] = assetPath,
|
|
116
|
+
["recursive"] = true,
|
|
117
|
+
["requested"] = importedPaths.Count,
|
|
118
|
+
["reimported"] = importedPaths.Count,
|
|
119
|
+
["skipped"] = 0,
|
|
120
|
+
["paths"] = new List<object>(importedPaths)
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
83
124
|
public static Dictionary<string, object> SaveImporterSettings(string requestedPath, AssetImporter importer, bool noReimport)
|
|
84
125
|
{
|
|
85
126
|
if (importer == null)
|
|
@@ -18,7 +18,10 @@ namespace UCP.Bridge
|
|
|
18
18
|
{
|
|
19
19
|
var parameters = ParseParameters(paramsJson);
|
|
20
20
|
var requestedPath = RequirePath(parameters);
|
|
21
|
-
|
|
21
|
+
var recursive = TryGetOptionalBool(parameters, "recursive");
|
|
22
|
+
return recursive
|
|
23
|
+
? AssetImportSupport.ReimportRecursive(requestedPath)
|
|
24
|
+
: AssetImportSupport.Reimport(requestedPath);
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
private static object HandleRead(string paramsJson)
|
|
@@ -45,6 +45,27 @@ namespace UCP.Bridge
|
|
|
45
45
|
return RecordLog(level, message, stackTrace);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
public static long GetLatestId()
|
|
49
|
+
{
|
|
50
|
+
lock (s_historyLock)
|
|
51
|
+
{
|
|
52
|
+
return s_history.Count == 0 ? 0 : s_history[s_history.Count - 1].Id;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public static Dictionary<string, object> BuildStatusSummary(long afterId = 0)
|
|
57
|
+
{
|
|
58
|
+
lock (s_historyLock)
|
|
59
|
+
{
|
|
60
|
+
var ordered = s_history
|
|
61
|
+
.Where(entry => entry.Id > afterId)
|
|
62
|
+
.OrderBy(entry => entry.Id)
|
|
63
|
+
.ToList();
|
|
64
|
+
|
|
65
|
+
return BuildStatusResult(ordered, afterId);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
48
69
|
private static object HandleTail(string paramsJson)
|
|
49
70
|
{
|
|
50
71
|
var query = ParseQuery(paramsJson, includePattern: false);
|
|
@@ -77,82 +98,12 @@ namespace UCP.Bridge
|
|
|
77
98
|
|
|
78
99
|
private static object HandleStatus(string paramsJson)
|
|
79
100
|
{
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
{
|
|
85
|
-
["info"] = ordered.Count(entry => entry.Level == "info"),
|
|
86
|
-
["warning"] = ordered.Count(entry => entry.Level == "warning"),
|
|
87
|
-
["error"] = ordered.Count(entry => entry.Level == "error"),
|
|
88
|
-
["exception"] = ordered.Count(entry => entry.Level == "exception")
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
var grouped = ordered
|
|
92
|
-
.GroupBy(entry => $"{entry.Level}|{Fingerprint(entry.Message)}")
|
|
93
|
-
.Select(group =>
|
|
94
|
-
{
|
|
95
|
-
var first = group.First();
|
|
96
|
-
var last = group.Last();
|
|
97
|
-
return new Dictionary<string, object>
|
|
98
|
-
{
|
|
99
|
-
["level"] = first.Level,
|
|
100
|
-
["fingerprint"] = Fingerprint(first.Message),
|
|
101
|
-
["sampleMessage"] = Preview(first.Message, MaxPreviewLength),
|
|
102
|
-
["count"] = group.Count(),
|
|
103
|
-
["firstTimestamp"] = first.Timestamp,
|
|
104
|
-
["lastTimestamp"] = last.Timestamp,
|
|
105
|
-
["latestId"] = last.Id
|
|
106
|
-
};
|
|
107
|
-
})
|
|
108
|
-
.OrderByDescending(entry => Convert.ToInt32(entry["count"]))
|
|
109
|
-
.ThenBy(entry => entry["sampleMessage"].ToString())
|
|
110
|
-
.ToList();
|
|
111
|
-
|
|
112
|
-
var result = new Dictionary<string, object>
|
|
113
|
-
{
|
|
114
|
-
["total"] = ordered.Count,
|
|
115
|
-
["byLevel"] = byLevel,
|
|
116
|
-
["uniqueCount"] = grouped.Count,
|
|
117
|
-
["topCategories"] = grouped.Take(8).Cast<object>().ToList()
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
if (ordered.Count > 0)
|
|
121
|
-
{
|
|
122
|
-
var first = ordered.First();
|
|
123
|
-
var last = ordered.Last();
|
|
124
|
-
result["firstTimestamp"] = first.Timestamp;
|
|
125
|
-
result["lastTimestamp"] = last.Timestamp;
|
|
126
|
-
result["historyWindowSeconds"] = Math.Max(0d, (last.TimestampUtc - first.TimestampUtc).TotalSeconds);
|
|
127
|
-
result["latestId"] = last.Id;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
var playSession = PlayModeController.GetSessionSnapshot();
|
|
131
|
-
result["play"] = SerializePlaySession(playSession);
|
|
132
|
-
|
|
133
|
-
if (playSession.LastEnteredPlayAtUtc.HasValue)
|
|
134
|
-
{
|
|
135
|
-
var sessionEnd = playSession.Playing
|
|
136
|
-
? DateTime.UtcNow
|
|
137
|
-
: (playSession.LastExitedPlayAtUtc ?? DateTime.UtcNow);
|
|
138
|
-
var sessionLogs = ordered
|
|
139
|
-
.Where(entry => entry.TimestampUtc >= playSession.LastEnteredPlayAtUtc.Value
|
|
140
|
-
&& entry.TimestampUtc <= sessionEnd)
|
|
141
|
-
.ToList();
|
|
142
|
-
|
|
143
|
-
result["lastPlayWindow"] = new Dictionary<string, object>
|
|
144
|
-
{
|
|
145
|
-
["startedAt"] = playSession.LastEnteredPlayAtUtc.Value.ToString("o"),
|
|
146
|
-
["endedAt"] = sessionEnd.ToString("o"),
|
|
147
|
-
["durationSeconds"] = Math.Max(0d, (sessionEnd - playSession.LastEnteredPlayAtUtc.Value).TotalSeconds),
|
|
148
|
-
["total"] = sessionLogs.Count,
|
|
149
|
-
["warnings"] = sessionLogs.Count(entry => entry.Level == "warning"),
|
|
150
|
-
["errors"] = sessionLogs.Count(entry => entry.Level == "error" || entry.Level == "exception")
|
|
151
|
-
};
|
|
152
|
-
}
|
|
101
|
+
var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
|
|
102
|
+
long afterId = 0;
|
|
103
|
+
if (p != null && p.TryGetValue("afterId", out var afterIdObj) && afterIdObj != null)
|
|
104
|
+
afterId = Math.Max(0, Convert.ToInt64(afterIdObj));
|
|
153
105
|
|
|
154
|
-
|
|
155
|
-
}
|
|
106
|
+
return BuildStatusSummary(afterId);
|
|
156
107
|
}
|
|
157
108
|
|
|
158
109
|
private static Dictionary<string, object> RecordLog(string level, string message, string stackTrace)
|
|
@@ -257,6 +208,83 @@ namespace UCP.Bridge
|
|
|
257
208
|
};
|
|
258
209
|
}
|
|
259
210
|
|
|
211
|
+
private static Dictionary<string, object> BuildStatusResult(List<LogRecord> ordered, long afterId)
|
|
212
|
+
{
|
|
213
|
+
var byLevel = new Dictionary<string, object>
|
|
214
|
+
{
|
|
215
|
+
["info"] = ordered.Count(entry => entry.Level == "info"),
|
|
216
|
+
["warning"] = ordered.Count(entry => entry.Level == "warning"),
|
|
217
|
+
["error"] = ordered.Count(entry => entry.Level == "error"),
|
|
218
|
+
["exception"] = ordered.Count(entry => entry.Level == "exception")
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
var grouped = ordered
|
|
222
|
+
.GroupBy(entry => $"{entry.Level}|{Fingerprint(entry.Message)}")
|
|
223
|
+
.Select(group =>
|
|
224
|
+
{
|
|
225
|
+
var first = group.First();
|
|
226
|
+
var last = group.Last();
|
|
227
|
+
return new Dictionary<string, object>
|
|
228
|
+
{
|
|
229
|
+
["level"] = first.Level,
|
|
230
|
+
["fingerprint"] = Fingerprint(first.Message),
|
|
231
|
+
["sampleMessage"] = Preview(first.Message, MaxPreviewLength),
|
|
232
|
+
["count"] = group.Count(),
|
|
233
|
+
["firstTimestamp"] = first.Timestamp,
|
|
234
|
+
["lastTimestamp"] = last.Timestamp,
|
|
235
|
+
["latestId"] = last.Id
|
|
236
|
+
};
|
|
237
|
+
})
|
|
238
|
+
.OrderByDescending(entry => Convert.ToInt32(entry["count"]))
|
|
239
|
+
.ThenBy(entry => entry["sampleMessage"].ToString())
|
|
240
|
+
.ToList();
|
|
241
|
+
|
|
242
|
+
var result = new Dictionary<string, object>
|
|
243
|
+
{
|
|
244
|
+
["afterId"] = afterId,
|
|
245
|
+
["total"] = ordered.Count,
|
|
246
|
+
["byLevel"] = byLevel,
|
|
247
|
+
["uniqueCount"] = grouped.Count,
|
|
248
|
+
["topCategories"] = grouped.Take(8).Cast<object>().ToList()
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
if (ordered.Count > 0)
|
|
252
|
+
{
|
|
253
|
+
var first = ordered.First();
|
|
254
|
+
var last = ordered.Last();
|
|
255
|
+
result["firstTimestamp"] = first.Timestamp;
|
|
256
|
+
result["lastTimestamp"] = last.Timestamp;
|
|
257
|
+
result["historyWindowSeconds"] = Math.Max(0d, (last.TimestampUtc - first.TimestampUtc).TotalSeconds);
|
|
258
|
+
result["latestId"] = last.Id;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
var playSession = PlayModeController.GetSessionSnapshot();
|
|
262
|
+
result["play"] = SerializePlaySession(playSession);
|
|
263
|
+
|
|
264
|
+
if (playSession.LastEnteredPlayAtUtc.HasValue)
|
|
265
|
+
{
|
|
266
|
+
var sessionEnd = playSession.Playing
|
|
267
|
+
? DateTime.UtcNow
|
|
268
|
+
: (playSession.LastExitedPlayAtUtc ?? DateTime.UtcNow);
|
|
269
|
+
var sessionLogs = ordered
|
|
270
|
+
.Where(entry => entry.TimestampUtc >= playSession.LastEnteredPlayAtUtc.Value
|
|
271
|
+
&& entry.TimestampUtc <= sessionEnd)
|
|
272
|
+
.ToList();
|
|
273
|
+
|
|
274
|
+
result["lastPlayWindow"] = new Dictionary<string, object>
|
|
275
|
+
{
|
|
276
|
+
["startedAt"] = playSession.LastEnteredPlayAtUtc.Value.ToString("o"),
|
|
277
|
+
["endedAt"] = sessionEnd.ToString("o"),
|
|
278
|
+
["durationSeconds"] = Math.Max(0d, (sessionEnd - playSession.LastEnteredPlayAtUtc.Value).TotalSeconds),
|
|
279
|
+
["total"] = sessionLogs.Count,
|
|
280
|
+
["warnings"] = sessionLogs.Count(entry => entry.Level == "warning"),
|
|
281
|
+
["errors"] = sessionLogs.Count(entry => entry.Level == "error" || entry.Level == "exception")
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
|
|
260
288
|
private static Dictionary<string, object> SerializeSummary(LogRecord entry)
|
|
261
289
|
{
|
|
262
290
|
return new Dictionary<string, object>
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using UnityEditor;
|
|
4
|
+
using UnityEngine;
|
|
5
|
+
|
|
6
|
+
namespace UCP.Bridge
|
|
7
|
+
{
|
|
8
|
+
/// <summary>
|
|
9
|
+
/// Bridge controller for reference search - used as a fallback when native Rust
|
|
10
|
+
/// indexing is not available (non-Force-Text projects) or for correctness verification.
|
|
11
|
+
/// </summary>
|
|
12
|
+
public static class ReferenceController
|
|
13
|
+
{
|
|
14
|
+
public static void Register(CommandRouter router)
|
|
15
|
+
{
|
|
16
|
+
router.Register("references/find", HandleFind);
|
|
17
|
+
router.Register("references/serialization-status", HandleSerializationStatus);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private static object HandleSerializationStatus(string paramsJson)
|
|
21
|
+
{
|
|
22
|
+
var settings = new SerializedObject(
|
|
23
|
+
AssetDatabase.LoadAllAssetsAtPath("ProjectSettings/EditorSettings.asset")[0]);
|
|
24
|
+
try
|
|
25
|
+
{
|
|
26
|
+
settings.Update();
|
|
27
|
+
var modeProp = settings.FindProperty("m_SerializationMode");
|
|
28
|
+
int mode = modeProp != null ? modeProp.intValue : -1;
|
|
29
|
+
|
|
30
|
+
return new Dictionary<string, object>
|
|
31
|
+
{
|
|
32
|
+
{ "serializationMode", mode },
|
|
33
|
+
{ "forceText", mode == 2 },
|
|
34
|
+
{ "visibleMetaFiles", EditorSettings.externalVersionControl == "Visible Meta Files" }
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
finally
|
|
38
|
+
{
|
|
39
|
+
settings.Dispose();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private static object HandleFind(string paramsJson)
|
|
44
|
+
{
|
|
45
|
+
var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
|
|
46
|
+
if (p == null)
|
|
47
|
+
throw new ArgumentException("Invalid parameters");
|
|
48
|
+
|
|
49
|
+
string targetGuid = null;
|
|
50
|
+
long targetFileId = 0;
|
|
51
|
+
int maxResults = 100;
|
|
52
|
+
|
|
53
|
+
if (p.TryGetValue("guid", out var gObj) && gObj != null)
|
|
54
|
+
targetGuid = gObj.ToString();
|
|
55
|
+
if (p.TryGetValue("fileId", out var fObj) && fObj != null)
|
|
56
|
+
targetFileId = Convert.ToInt64(fObj);
|
|
57
|
+
if (p.TryGetValue("maxResults", out var mObj) && mObj != null)
|
|
58
|
+
maxResults = Convert.ToInt32(mObj);
|
|
59
|
+
|
|
60
|
+
if (string.IsNullOrEmpty(targetGuid))
|
|
61
|
+
throw new ArgumentException("Missing 'guid' parameter");
|
|
62
|
+
|
|
63
|
+
var targetPath = AssetDatabase.GUIDToAssetPath(targetGuid);
|
|
64
|
+
var references = new List<object>();
|
|
65
|
+
|
|
66
|
+
// Phase 1: Find all assets that depend on the target using AssetDatabase
|
|
67
|
+
var allAssets = AssetDatabase.GetAllAssetPaths();
|
|
68
|
+
var dependentPaths = new List<string>();
|
|
69
|
+
|
|
70
|
+
foreach (var assetPath in allAssets)
|
|
71
|
+
{
|
|
72
|
+
if (!assetPath.StartsWith("Assets/") && !assetPath.StartsWith("Packages/"))
|
|
73
|
+
continue;
|
|
74
|
+
|
|
75
|
+
var deps = AssetDatabase.GetDependencies(assetPath, false);
|
|
76
|
+
foreach (var dep in deps)
|
|
77
|
+
{
|
|
78
|
+
if (dep == targetPath)
|
|
79
|
+
{
|
|
80
|
+
dependentPaths.Add(assetPath);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Phase 2: Walk SerializedObject properties to find exact reference locations
|
|
87
|
+
foreach (var sourcePath in dependentPaths)
|
|
88
|
+
{
|
|
89
|
+
if (references.Count >= maxResults)
|
|
90
|
+
break;
|
|
91
|
+
|
|
92
|
+
var assets = AssetDatabase.LoadAllAssetsAtPath(sourcePath);
|
|
93
|
+
foreach (var asset in assets)
|
|
94
|
+
{
|
|
95
|
+
if (asset == null) continue;
|
|
96
|
+
if (references.Count >= maxResults) break;
|
|
97
|
+
|
|
98
|
+
var so = new SerializedObject(asset);
|
|
99
|
+
try
|
|
100
|
+
{
|
|
101
|
+
so.Update();
|
|
102
|
+
var iterator = so.GetIterator();
|
|
103
|
+
bool enterChildren = true;
|
|
104
|
+
|
|
105
|
+
while (iterator.Next(enterChildren))
|
|
106
|
+
{
|
|
107
|
+
enterChildren = true;
|
|
108
|
+
|
|
109
|
+
if (iterator.propertyType != SerializedPropertyType.ObjectReference)
|
|
110
|
+
continue;
|
|
111
|
+
|
|
112
|
+
var refValue = iterator.objectReferenceValue;
|
|
113
|
+
if (refValue == null) continue;
|
|
114
|
+
|
|
115
|
+
string refPath = AssetDatabase.GetAssetPath(refValue);
|
|
116
|
+
if (string.IsNullOrEmpty(refPath)) continue;
|
|
117
|
+
|
|
118
|
+
string refGuid = AssetDatabase.AssetPathToGUID(refPath);
|
|
119
|
+
if (refGuid != targetGuid) continue;
|
|
120
|
+
|
|
121
|
+
// If a specific fileId was requested, check it
|
|
122
|
+
if (targetFileId != 0)
|
|
123
|
+
{
|
|
124
|
+
if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(
|
|
125
|
+
refValue, out string _, out long localId))
|
|
126
|
+
continue;
|
|
127
|
+
if (localId != targetFileId)
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
references.Add(new Dictionary<string, object>
|
|
132
|
+
{
|
|
133
|
+
{ "sourcePath", sourcePath },
|
|
134
|
+
{ "sourceObjectName", asset.name },
|
|
135
|
+
{ "sourceObjectType", asset.GetType().Name },
|
|
136
|
+
{ "propertyPath", iterator.propertyPath },
|
|
137
|
+
{ "targetGuid", targetGuid },
|
|
138
|
+
{ "targetPath", targetPath },
|
|
139
|
+
{ "referencedObjectName", refValue.name },
|
|
140
|
+
{ "referencedObjectType", refValue.GetType().Name }
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (references.Count >= maxResults)
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
finally
|
|
148
|
+
{
|
|
149
|
+
so.Dispose();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return new Dictionary<string, object>
|
|
155
|
+
{
|
|
156
|
+
{ "targetGuid", targetGuid },
|
|
157
|
+
{ "targetPath", targetPath ?? "" },
|
|
158
|
+
{ "count", references.Count },
|
|
159
|
+
{ "references", references }
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -44,20 +44,26 @@ namespace UCP.Bridge
|
|
|
44
44
|
throw new System.ArgumentException("Missing 'path' parameter");
|
|
45
45
|
|
|
46
46
|
var path = pathObj.ToString();
|
|
47
|
+
var additive = GetBoolParam(p, "additive", false);
|
|
47
48
|
var saveDirtyScenes = GetBoolParam(p, "saveDirtyScenes", true);
|
|
48
49
|
var discardUntitled = GetBoolParam(p, "discardUntitled", true);
|
|
49
50
|
|
|
50
51
|
if (EditorApplication.isPlaying)
|
|
51
52
|
{
|
|
52
|
-
SceneManager.LoadScene(path);
|
|
53
|
+
SceneManager.LoadScene(path, additive ? LoadSceneMode.Additive : LoadSceneMode.Single);
|
|
53
54
|
}
|
|
54
55
|
else
|
|
55
56
|
{
|
|
56
57
|
SaveDirtyScenesIfRequested(saveDirtyScenes, discardUntitled);
|
|
57
|
-
EditorSceneManager.OpenScene(path, OpenSceneMode.Single);
|
|
58
|
+
EditorSceneManager.OpenScene(path, additive ? OpenSceneMode.Additive : OpenSceneMode.Single);
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
return new
|
|
61
|
+
return new Dictionary<string, object>
|
|
62
|
+
{
|
|
63
|
+
["status"] = "ok",
|
|
64
|
+
["loaded"] = path,
|
|
65
|
+
["additive"] = additive
|
|
66
|
+
};
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
private static bool GetBoolParam(Dictionary<string, object> parameters, string key, bool defaultValue)
|
|
@@ -11,6 +11,7 @@ namespace UCP.Bridge
|
|
|
11
11
|
{
|
|
12
12
|
private static TestRunnerApi s_api;
|
|
13
13
|
private static TestResultCollector s_collector;
|
|
14
|
+
private const string ConsoleGuardName = "UCP.Bridge.Tests.ConsoleLogGuard";
|
|
14
15
|
|
|
15
16
|
public static void Register(CommandRouter router)
|
|
16
17
|
{
|
|
@@ -42,7 +43,8 @@ namespace UCP.Bridge
|
|
|
42
43
|
if (s_collector != null)
|
|
43
44
|
s_api.UnregisterCallbacks(s_collector);
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
var logCursor = LogsController.GetLatestId();
|
|
47
|
+
s_collector = new TestResultCollector(logCursor);
|
|
46
48
|
s_api.RegisterCallbacks(s_collector);
|
|
47
49
|
|
|
48
50
|
var executionSettings = new ExecutionSettings
|
|
@@ -130,11 +132,13 @@ namespace UCP.Bridge
|
|
|
130
132
|
private class TestResultCollector : ICallbacks
|
|
131
133
|
{
|
|
132
134
|
private readonly List<object> _results = new();
|
|
135
|
+
private readonly long _logCursor;
|
|
133
136
|
private int _passed, _failed, _skipped;
|
|
134
137
|
private double _startTime;
|
|
135
138
|
|
|
136
|
-
public TestResultCollector()
|
|
139
|
+
public TestResultCollector(long logCursor)
|
|
137
140
|
{
|
|
141
|
+
_logCursor = logCursor;
|
|
138
142
|
_startTime = EditorApplication.timeSinceStartup;
|
|
139
143
|
}
|
|
140
144
|
|
|
@@ -147,6 +151,21 @@ namespace UCP.Bridge
|
|
|
147
151
|
_failed = 0;
|
|
148
152
|
_skipped = 0;
|
|
149
153
|
CollectLeafResults(result);
|
|
154
|
+
var logSummary = LogsController.BuildStatusSummary(_logCursor);
|
|
155
|
+
var consoleWarnings = GetLevelCount(logSummary, "warning");
|
|
156
|
+
var consoleErrors = GetLevelCount(logSummary, "error") + GetLevelCount(logSummary, "exception");
|
|
157
|
+
|
|
158
|
+
if (consoleErrors > 0)
|
|
159
|
+
{
|
|
160
|
+
_failed++;
|
|
161
|
+
_results.Add(new Dictionary<string, object>
|
|
162
|
+
{
|
|
163
|
+
["name"] = ConsoleGuardName,
|
|
164
|
+
["status"] = "failed",
|
|
165
|
+
["duration"] = 0.0,
|
|
166
|
+
["message"] = BuildConsoleGuardMessage(consoleErrors, consoleWarnings, logSummary)
|
|
167
|
+
});
|
|
168
|
+
}
|
|
150
169
|
|
|
151
170
|
var duration = EditorApplication.timeSinceStartup - _startTime;
|
|
152
171
|
|
|
@@ -156,13 +175,17 @@ namespace UCP.Bridge
|
|
|
156
175
|
["passed"] = _passed,
|
|
157
176
|
["failed"] = _failed,
|
|
158
177
|
["skipped"] = _skipped,
|
|
159
|
-
["duration"] = (double)duration
|
|
178
|
+
["duration"] = (double)duration,
|
|
179
|
+
["consoleClean"] = consoleErrors == 0,
|
|
180
|
+
["consoleWarnings"] = consoleWarnings,
|
|
181
|
+
["consoleErrors"] = consoleErrors
|
|
160
182
|
};
|
|
161
183
|
|
|
162
184
|
BridgeServer.BroadcastNotification("tests/result", new Dictionary<string, object>
|
|
163
185
|
{
|
|
164
186
|
["summary"] = summary,
|
|
165
|
-
["tests"] = _results
|
|
187
|
+
["tests"] = _results,
|
|
188
|
+
["logs"] = logSummary
|
|
166
189
|
});
|
|
167
190
|
|
|
168
191
|
if (s_api != null)
|
|
@@ -235,6 +258,59 @@ namespace UCP.Bridge
|
|
|
235
258
|
|
|
236
259
|
return result.Name;
|
|
237
260
|
}
|
|
261
|
+
|
|
262
|
+
private static int GetLevelCount(Dictionary<string, object> logSummary, string level)
|
|
263
|
+
{
|
|
264
|
+
if (!logSummary.TryGetValue("byLevel", out var byLevelObj)
|
|
265
|
+
|| byLevelObj is not Dictionary<string, object> byLevel
|
|
266
|
+
|| !byLevel.TryGetValue(level, out var value))
|
|
267
|
+
{
|
|
268
|
+
return 0;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return Convert.ToInt32(value);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private static string BuildConsoleGuardMessage(
|
|
275
|
+
int consoleErrors,
|
|
276
|
+
int consoleWarnings,
|
|
277
|
+
Dictionary<string, object> logSummary)
|
|
278
|
+
{
|
|
279
|
+
var message = $"Unity emitted {consoleErrors} error/exception log(s) during the test run";
|
|
280
|
+
if (consoleWarnings > 0)
|
|
281
|
+
message += $" and {consoleWarnings} warning log(s)";
|
|
282
|
+
|
|
283
|
+
if (!logSummary.TryGetValue("topCategories", out var categoriesObj)
|
|
284
|
+
|| categoriesObj is not List<object> categories
|
|
285
|
+
|| categories.Count == 0)
|
|
286
|
+
{
|
|
287
|
+
return message + ".";
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
var parts = new List<string>();
|
|
291
|
+
foreach (var categoryObj in categories)
|
|
292
|
+
{
|
|
293
|
+
if (categoryObj is not Dictionary<string, object> category)
|
|
294
|
+
continue;
|
|
295
|
+
|
|
296
|
+
var count = category.TryGetValue("count", out var countObj)
|
|
297
|
+
? Convert.ToInt32(countObj)
|
|
298
|
+
: 0;
|
|
299
|
+
var sample = category.TryGetValue("sampleMessage", out var sampleObj)
|
|
300
|
+
? sampleObj?.ToString() ?? string.Empty
|
|
301
|
+
: string.Empty;
|
|
302
|
+
if (count <= 0 || string.IsNullOrEmpty(sample))
|
|
303
|
+
continue;
|
|
304
|
+
|
|
305
|
+
parts.Add($"{count}x {sample}");
|
|
306
|
+
if (parts.Count >= 3)
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return parts.Count == 0
|
|
311
|
+
? message + "."
|
|
312
|
+
: $"{message}. Top categories: {string.Join(" | ", parts)}";
|
|
313
|
+
}
|
|
238
314
|
}
|
|
239
315
|
}
|
|
240
316
|
}
|