@mflrevan/ucp 0.5.0 → 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 +1 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/AssetController.cs +236 -35
- 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/SceneController.cs +9 -3
- package/bridge/com.ucp.bridge/Editor/Controllers/TestRunnerController.cs +80 -4
- package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +209 -0
- package/bridge/com.ucp.bridge/package.json +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @mflrevan/ucp
|
|
2
2
|
|
|
3
|
-
Version `0.5.
|
|
3
|
+
Version `0.5.1` of the Unity Control Protocol CLI.
|
|
4
4
|
|
|
5
5
|
This package installs the `ucp` command, downloads the matching published binary for your platform during `postinstall`, and ships the matching Unity bridge payload inside the npm package.
|
|
6
6
|
|
|
@@ -26,7 +26,7 @@ namespace UCP.Bridge
|
|
|
26
26
|
private const int DefaultPort = 21342;
|
|
27
27
|
private const int MaxPort = 21352;
|
|
28
28
|
private const int MaxConnections = 4;
|
|
29
|
-
private const string ProtocolVersion = "0.5.
|
|
29
|
+
private const string ProtocolVersion = "0.5.1";
|
|
30
30
|
|
|
31
31
|
private static TcpListener s_listener;
|
|
32
32
|
private static CancellationTokenSource s_cts;
|
|
@@ -2,6 +2,7 @@ using System;
|
|
|
2
2
|
using System.Collections.Generic;
|
|
3
3
|
using System.IO;
|
|
4
4
|
using System.Reflection;
|
|
5
|
+
using System.Text.RegularExpressions;
|
|
5
6
|
using UnityEditor;
|
|
6
7
|
using UnityEngine;
|
|
7
8
|
|
|
@@ -28,6 +29,7 @@ namespace UCP.Bridge
|
|
|
28
29
|
string typeFilter = null;
|
|
29
30
|
string nameFilter = null;
|
|
30
31
|
string pathFilter = null;
|
|
32
|
+
bool useRegex = false;
|
|
31
33
|
int maxResults = 100;
|
|
32
34
|
|
|
33
35
|
if (p != null)
|
|
@@ -38,13 +40,28 @@ namespace UCP.Bridge
|
|
|
38
40
|
nameFilter = nObj.ToString();
|
|
39
41
|
if (p.TryGetValue("path", out var pObj) && pObj != null)
|
|
40
42
|
pathFilter = pObj.ToString();
|
|
43
|
+
if (p.TryGetValue("regex", out var regexObj) && regexObj != null)
|
|
44
|
+
useRegex = Convert.ToBoolean(regexObj);
|
|
41
45
|
if (p.TryGetValue("maxResults", out var mObj))
|
|
42
46
|
maxResults = Convert.ToInt32(mObj);
|
|
43
47
|
}
|
|
44
48
|
|
|
49
|
+
Regex nameRegex = null;
|
|
50
|
+
if (useRegex && !string.IsNullOrEmpty(nameFilter))
|
|
51
|
+
{
|
|
52
|
+
try
|
|
53
|
+
{
|
|
54
|
+
nameRegex = new Regex(nameFilter, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
|
55
|
+
}
|
|
56
|
+
catch (ArgumentException ex)
|
|
57
|
+
{
|
|
58
|
+
throw new ArgumentException($"Invalid regex for name filter: {ex.Message}");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
45
62
|
// Build search filter
|
|
46
63
|
string filter = "";
|
|
47
|
-
if (!string.IsNullOrEmpty(nameFilter))
|
|
64
|
+
if (!useRegex && !string.IsNullOrEmpty(nameFilter))
|
|
48
65
|
filter += nameFilter + " ";
|
|
49
66
|
if (!string.IsNullOrEmpty(typeFilter))
|
|
50
67
|
filter += $"t:{GetSearchTypeFilter(typeFilter)}";
|
|
@@ -64,7 +81,7 @@ namespace UCP.Bridge
|
|
|
64
81
|
for (int i = 0; i < guids.Length; i++)
|
|
65
82
|
{
|
|
66
83
|
string assetPath = AssetDatabase.GUIDToAssetPath(guids[i]);
|
|
67
|
-
foreach (var asset in GetMatchingAssets(assetPath, typeFilter, nameFilter))
|
|
84
|
+
foreach (var asset in GetMatchingAssets(assetPath, typeFilter, nameFilter, nameRegex))
|
|
68
85
|
{
|
|
69
86
|
totalMatches++;
|
|
70
87
|
if (results.Count >= maxResults)
|
|
@@ -354,6 +371,7 @@ namespace UCP.Bridge
|
|
|
354
371
|
throw new ArgumentException("Missing 'moves' parameter");
|
|
355
372
|
|
|
356
373
|
var continueOnError = TryGetOptionalBool(parameters, "continueOnError");
|
|
374
|
+
var dryRun = TryGetOptionalBool(parameters, "dryRun");
|
|
357
375
|
var requests = ParseMoveRequests(movesObject);
|
|
358
376
|
var results = new List<object>();
|
|
359
377
|
var errors = new List<object>();
|
|
@@ -361,15 +379,14 @@ namespace UCP.Bridge
|
|
|
361
379
|
var stopped = false;
|
|
362
380
|
var anyChanged = false;
|
|
363
381
|
|
|
364
|
-
|
|
365
|
-
try
|
|
382
|
+
if (dryRun)
|
|
366
383
|
{
|
|
367
384
|
for (var index = 0; index < requests.Count; index++)
|
|
368
385
|
{
|
|
369
386
|
var request = requests[index];
|
|
370
387
|
try
|
|
371
388
|
{
|
|
372
|
-
var moveResult =
|
|
389
|
+
var moveResult = PreviewMoveInternal(request.SourcePath, request.Destination);
|
|
373
390
|
results.Add(AddMoveIndex(moveResult, index));
|
|
374
391
|
if (GetBool(moveResult, "changed"))
|
|
375
392
|
{
|
|
@@ -395,12 +412,75 @@ namespace UCP.Bridge
|
|
|
395
412
|
}
|
|
396
413
|
}
|
|
397
414
|
}
|
|
398
|
-
|
|
415
|
+
else
|
|
399
416
|
{
|
|
400
|
-
|
|
417
|
+
var preparedMoves = new List<PreparedMove>();
|
|
418
|
+
for (var index = 0; index < requests.Count; index++)
|
|
419
|
+
{
|
|
420
|
+
var request = requests[index];
|
|
421
|
+
try
|
|
422
|
+
{
|
|
423
|
+
var preparedMove = PrepareMove(request.SourcePath, request.Destination, true);
|
|
424
|
+
preparedMoves.Add(new PreparedMove(index, request.SourcePath, preparedMove));
|
|
425
|
+
}
|
|
426
|
+
catch (Exception ex)
|
|
427
|
+
{
|
|
428
|
+
errors.Add(new Dictionary<string, object>
|
|
429
|
+
{
|
|
430
|
+
["index"] = index,
|
|
431
|
+
["sourcePath"] = request.SourcePath,
|
|
432
|
+
["destinationPath"] = request.Destination,
|
|
433
|
+
["message"] = ex.Message
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
if (!continueOnError)
|
|
437
|
+
{
|
|
438
|
+
stopped = true;
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
AssetDatabase.StartAssetEditing();
|
|
445
|
+
try
|
|
446
|
+
{
|
|
447
|
+
foreach (var preparedMove in preparedMoves)
|
|
448
|
+
{
|
|
449
|
+
try
|
|
450
|
+
{
|
|
451
|
+
var moveResult = MovePreparedAsset(preparedMove.SourcePath, preparedMove.Move, false);
|
|
452
|
+
results.Add(AddMoveIndex(moveResult, preparedMove.Index));
|
|
453
|
+
if (GetBool(moveResult, "changed"))
|
|
454
|
+
{
|
|
455
|
+
movedCount++;
|
|
456
|
+
anyChanged = true;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
catch (Exception ex)
|
|
460
|
+
{
|
|
461
|
+
errors.Add(new Dictionary<string, object>
|
|
462
|
+
{
|
|
463
|
+
["index"] = preparedMove.Index,
|
|
464
|
+
["sourcePath"] = preparedMove.SourcePath,
|
|
465
|
+
["destinationPath"] = preparedMove.Move.ResolvedDestination,
|
|
466
|
+
["message"] = ex.Message
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
if (!continueOnError)
|
|
470
|
+
{
|
|
471
|
+
stopped = true;
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
finally
|
|
478
|
+
{
|
|
479
|
+
AssetDatabase.StopAssetEditing();
|
|
480
|
+
}
|
|
401
481
|
}
|
|
402
482
|
|
|
403
|
-
if (anyChanged)
|
|
483
|
+
if (!dryRun && anyChanged)
|
|
404
484
|
{
|
|
405
485
|
AssetDatabase.SaveAssets();
|
|
406
486
|
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
|
@@ -412,6 +492,7 @@ namespace UCP.Bridge
|
|
|
412
492
|
["requested"] = requests.Count,
|
|
413
493
|
["moved"] = movedCount,
|
|
414
494
|
["failed"] = errors.Count,
|
|
495
|
+
["dryRun"] = dryRun,
|
|
415
496
|
["stopped"] = stopped,
|
|
416
497
|
["results"] = results,
|
|
417
498
|
["errors"] = errors
|
|
@@ -431,10 +512,19 @@ namespace UCP.Bridge
|
|
|
431
512
|
}
|
|
432
513
|
}
|
|
433
514
|
|
|
434
|
-
private static IEnumerable<UnityEngine.Object> GetMatchingAssets(
|
|
515
|
+
private static IEnumerable<UnityEngine.Object> GetMatchingAssets(
|
|
516
|
+
string assetPath,
|
|
517
|
+
string typeFilter,
|
|
518
|
+
string nameFilter,
|
|
519
|
+
Regex nameRegex)
|
|
435
520
|
{
|
|
436
521
|
UnityEngine.Object[] candidates;
|
|
437
|
-
if (string.IsNullOrEmpty(typeFilter) && string.IsNullOrEmpty(nameFilter))
|
|
522
|
+
if (string.IsNullOrEmpty(typeFilter) && string.IsNullOrEmpty(nameFilter) && nameRegex == null)
|
|
523
|
+
{
|
|
524
|
+
var mainAsset = AssetDatabase.LoadMainAssetAtPath(assetPath);
|
|
525
|
+
candidates = mainAsset != null ? new[] { mainAsset } : Array.Empty<UnityEngine.Object>();
|
|
526
|
+
}
|
|
527
|
+
else if (assetPath.EndsWith(".unity", StringComparison.OrdinalIgnoreCase))
|
|
438
528
|
{
|
|
439
529
|
var mainAsset = AssetDatabase.LoadMainAssetAtPath(assetPath);
|
|
440
530
|
candidates = mainAsset != null ? new[] { mainAsset } : Array.Empty<UnityEngine.Object>();
|
|
@@ -447,17 +537,24 @@ namespace UCP.Bridge
|
|
|
447
537
|
foreach (var candidate in candidates)
|
|
448
538
|
{
|
|
449
539
|
if (candidate == null) continue;
|
|
450
|
-
if (!MatchesName(candidate, assetPath, nameFilter)) continue;
|
|
540
|
+
if (!MatchesName(candidate, assetPath, nameFilter, nameRegex)) continue;
|
|
451
541
|
if (!MatchesType(candidate, assetPath, typeFilter)) continue;
|
|
452
542
|
yield return candidate;
|
|
453
543
|
}
|
|
454
544
|
}
|
|
455
545
|
|
|
456
|
-
private static bool MatchesName(UnityEngine.Object asset, string assetPath, string nameFilter)
|
|
546
|
+
private static bool MatchesName(UnityEngine.Object asset, string assetPath, string nameFilter, Regex nameRegex)
|
|
457
547
|
{
|
|
458
|
-
if (string.IsNullOrEmpty(nameFilter))
|
|
548
|
+
if (string.IsNullOrEmpty(nameFilter) && nameRegex == null)
|
|
459
549
|
return true;
|
|
460
550
|
|
|
551
|
+
if (nameRegex != null)
|
|
552
|
+
{
|
|
553
|
+
var fileStem = System.IO.Path.GetFileNameWithoutExtension(assetPath);
|
|
554
|
+
return nameRegex.IsMatch(asset.name)
|
|
555
|
+
|| (!string.IsNullOrEmpty(fileStem) && nameRegex.IsMatch(fileStem));
|
|
556
|
+
}
|
|
557
|
+
|
|
461
558
|
return asset.name.Contains(nameFilter, StringComparison.OrdinalIgnoreCase)
|
|
462
559
|
|| System.IO.Path.GetFileNameWithoutExtension(assetPath).Contains(nameFilter, StringComparison.OrdinalIgnoreCase);
|
|
463
560
|
}
|
|
@@ -507,30 +604,29 @@ namespace UCP.Bridge
|
|
|
507
604
|
|
|
508
605
|
private static Dictionary<string, object> MoveAssetInternal(string sourcePath, string destination, bool finalize)
|
|
509
606
|
{
|
|
510
|
-
var
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
throw new ArgumentException($"Asset not found: {normalizedSource}");
|
|
514
|
-
|
|
515
|
-
if (!IsMovableAssetPath(normalizedSource))
|
|
516
|
-
throw new ArgumentException($"Asset moves are only supported under Assets/: {normalizedSource}");
|
|
517
|
-
|
|
518
|
-
var identity = DescribeExistingAsset(normalizedSource, isFolder);
|
|
519
|
-
var resolvedDestination = ResolveDestinationPath(normalizedSource, destination);
|
|
520
|
-
if (!IsMovableAssetPath(resolvedDestination))
|
|
521
|
-
throw new ArgumentException($"Destination must be under Assets/: {resolvedDestination}");
|
|
522
|
-
|
|
523
|
-
if (string.Equals(normalizedSource, resolvedDestination, StringComparison.OrdinalIgnoreCase))
|
|
524
|
-
{
|
|
525
|
-
return DescribeMovedAsset(normalizedSource, resolvedDestination, false, identity);
|
|
526
|
-
}
|
|
607
|
+
var move = PrepareMove(sourcePath, destination, true);
|
|
608
|
+
return MovePreparedAsset(NormalizeMovePath(sourcePath), move, finalize);
|
|
609
|
+
}
|
|
527
610
|
|
|
528
|
-
|
|
529
|
-
|
|
611
|
+
private static Dictionary<string, object> PreviewMoveInternal(string sourcePath, string destination)
|
|
612
|
+
{
|
|
613
|
+
var normalizedSource = NormalizeMovePath(sourcePath);
|
|
614
|
+
var move = PrepareMove(normalizedSource, destination, false);
|
|
615
|
+
return DescribeMovedAsset(normalizedSource, move.ResolvedDestination, move.Changed, move.Identity);
|
|
616
|
+
}
|
|
530
617
|
|
|
531
|
-
|
|
618
|
+
private static ValidatedMove PrepareMove(string sourcePath, string destination, bool ensureParentFolders)
|
|
619
|
+
{
|
|
620
|
+
var normalizedSource = NormalizeMovePath(sourcePath);
|
|
621
|
+
var move = ValidateMoveRequest(normalizedSource, destination);
|
|
622
|
+
if (ensureParentFolders)
|
|
623
|
+
EnsureParentFoldersExist(move.ResolvedDestination);
|
|
624
|
+
return move;
|
|
625
|
+
}
|
|
532
626
|
|
|
533
|
-
|
|
627
|
+
private static Dictionary<string, object> MovePreparedAsset(string normalizedSource, ValidatedMove move, bool finalize)
|
|
628
|
+
{
|
|
629
|
+
var moveError = AssetDatabase.MoveAsset(normalizedSource, move.ResolvedDestination);
|
|
534
630
|
if (!string.IsNullOrEmpty(moveError))
|
|
535
631
|
throw new InvalidOperationException(moveError);
|
|
536
632
|
|
|
@@ -540,7 +636,7 @@ namespace UCP.Bridge
|
|
|
540
636
|
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
|
541
637
|
}
|
|
542
638
|
|
|
543
|
-
return DescribeMovedAsset(normalizedSource,
|
|
639
|
+
return DescribeMovedAsset(normalizedSource, move.ResolvedDestination, true, move.Identity);
|
|
544
640
|
}
|
|
545
641
|
|
|
546
642
|
private static Dictionary<string, object> DescribeMovedAsset(
|
|
@@ -648,6 +744,29 @@ namespace UCP.Bridge
|
|
|
648
744
|
return path?.Trim().Replace('\\', '/');
|
|
649
745
|
}
|
|
650
746
|
|
|
747
|
+
private static ValidatedMove ValidateMoveRequest(string normalizedSource, string destination)
|
|
748
|
+
{
|
|
749
|
+
var isFolder = AssetDatabase.IsValidFolder(normalizedSource);
|
|
750
|
+
if (!isFolder && !AssetDatabase.AssetPathExists(normalizedSource))
|
|
751
|
+
throw new ArgumentException(BuildAssetNotFoundMessage(normalizedSource));
|
|
752
|
+
|
|
753
|
+
if (!IsMovableAssetPath(normalizedSource))
|
|
754
|
+
throw new ArgumentException($"Asset moves are only supported under Assets/: {normalizedSource}");
|
|
755
|
+
|
|
756
|
+
var identity = DescribeExistingAsset(normalizedSource, isFolder);
|
|
757
|
+
var resolvedDestination = ResolveDestinationPath(normalizedSource, destination);
|
|
758
|
+
if (!IsMovableAssetPath(resolvedDestination))
|
|
759
|
+
throw new ArgumentException($"Destination must be under Assets/: {resolvedDestination}");
|
|
760
|
+
|
|
761
|
+
if (string.Equals(normalizedSource, resolvedDestination, StringComparison.OrdinalIgnoreCase))
|
|
762
|
+
return new ValidatedMove(identity, resolvedDestination, false);
|
|
763
|
+
|
|
764
|
+
if (AssetDatabase.AssetPathExists(resolvedDestination) || AssetDatabase.IsValidFolder(resolvedDestination))
|
|
765
|
+
throw new ArgumentException($"Destination already exists: {resolvedDestination}");
|
|
766
|
+
|
|
767
|
+
return new ValidatedMove(identity, resolvedDestination, true);
|
|
768
|
+
}
|
|
769
|
+
|
|
651
770
|
private static bool IsMovableAssetPath(string path)
|
|
652
771
|
{
|
|
653
772
|
return !string.IsNullOrEmpty(path)
|
|
@@ -655,6 +774,60 @@ namespace UCP.Bridge
|
|
|
655
774
|
|| path.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase));
|
|
656
775
|
}
|
|
657
776
|
|
|
777
|
+
private static string BuildAssetNotFoundMessage(string normalizedSource)
|
|
778
|
+
{
|
|
779
|
+
var message = $"Asset not found: {normalizedSource}.";
|
|
780
|
+
var suggestions = FindSimilarAssetPaths(normalizedSource, 3);
|
|
781
|
+
if (suggestions.Count > 0)
|
|
782
|
+
message += " Did you mean: " + string.Join(", ", suggestions) + "?";
|
|
783
|
+
message += " If the asset was created or renamed outside Unity, refresh or reimport so AssetDatabase can pick up the latest path.";
|
|
784
|
+
return message;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
private static List<string> FindSimilarAssetPaths(string normalizedSource, int maxSuggestions)
|
|
788
|
+
{
|
|
789
|
+
var targetName = Path.GetFileNameWithoutExtension(normalizedSource) ?? normalizedSource;
|
|
790
|
+
var targetFileName = Path.GetFileName(normalizedSource) ?? normalizedSource;
|
|
791
|
+
var targetExtension = Path.GetExtension(normalizedSource) ?? string.Empty;
|
|
792
|
+
var matches = new List<(string path, int score)>();
|
|
793
|
+
|
|
794
|
+
foreach (var candidate in AssetDatabase.GetAllAssetPaths())
|
|
795
|
+
{
|
|
796
|
+
if (!candidate.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
|
|
797
|
+
continue;
|
|
798
|
+
|
|
799
|
+
var score = 0;
|
|
800
|
+
if (candidate.IndexOf(targetName, StringComparison.OrdinalIgnoreCase) >= 0)
|
|
801
|
+
score += 3;
|
|
802
|
+
if (string.Equals(Path.GetFileName(candidate), targetFileName, StringComparison.OrdinalIgnoreCase))
|
|
803
|
+
score += 4;
|
|
804
|
+
if (string.Equals(Path.GetExtension(candidate), targetExtension, StringComparison.OrdinalIgnoreCase))
|
|
805
|
+
score += 1;
|
|
806
|
+
if (score > 0)
|
|
807
|
+
matches.Add((candidate, score));
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
matches.Sort((left, right) =>
|
|
811
|
+
{
|
|
812
|
+
var scoreCompare = right.score.CompareTo(left.score);
|
|
813
|
+
return scoreCompare != 0
|
|
814
|
+
? scoreCompare
|
|
815
|
+
: string.Compare(left.path, right.path, StringComparison.OrdinalIgnoreCase);
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
var suggestions = new List<string>();
|
|
819
|
+
foreach (var match in matches)
|
|
820
|
+
{
|
|
821
|
+
if (suggestions.Contains(match.path))
|
|
822
|
+
continue;
|
|
823
|
+
suggestions.Add(match.path);
|
|
824
|
+
if (suggestions.Count >= maxSuggestions)
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return suggestions;
|
|
829
|
+
}
|
|
830
|
+
|
|
658
831
|
private static Dictionary<string, object> ParseParameters(string paramsJson)
|
|
659
832
|
{
|
|
660
833
|
return MiniJson.Deserialize(paramsJson) as Dictionary<string, object>
|
|
@@ -750,6 +923,20 @@ namespace UCP.Bridge
|
|
|
750
923
|
public string Destination { get; }
|
|
751
924
|
}
|
|
752
925
|
|
|
926
|
+
private readonly struct PreparedMove
|
|
927
|
+
{
|
|
928
|
+
public PreparedMove(int index, string sourcePath, ValidatedMove move)
|
|
929
|
+
{
|
|
930
|
+
Index = index;
|
|
931
|
+
SourcePath = sourcePath;
|
|
932
|
+
Move = move;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
public int Index { get; }
|
|
936
|
+
public string SourcePath { get; }
|
|
937
|
+
public ValidatedMove Move { get; }
|
|
938
|
+
}
|
|
939
|
+
|
|
753
940
|
private readonly struct AssetIdentity
|
|
754
941
|
{
|
|
755
942
|
public AssetIdentity(string guid, string name, string type, bool isFolder)
|
|
@@ -766,5 +953,19 @@ namespace UCP.Bridge
|
|
|
766
953
|
public bool IsFolder { get; }
|
|
767
954
|
}
|
|
768
955
|
|
|
956
|
+
private readonly struct ValidatedMove
|
|
957
|
+
{
|
|
958
|
+
public ValidatedMove(AssetIdentity identity, string resolvedDestination, bool changed)
|
|
959
|
+
{
|
|
960
|
+
Identity = identity;
|
|
961
|
+
ResolvedDestination = resolvedDestination;
|
|
962
|
+
Changed = changed;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
public AssetIdentity Identity { get; }
|
|
966
|
+
public string ResolvedDestination { get; }
|
|
967
|
+
public bool Changed { get; }
|
|
968
|
+
}
|
|
969
|
+
|
|
769
970
|
}
|
|
770
971
|
}
|
|
@@ -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>
|
|
@@ -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
|
}
|
|
@@ -22,11 +22,14 @@ namespace UCP.Bridge.Tests
|
|
|
22
22
|
private const string TempScriptPath = "Assets/UcpControllerSmokeComponent.cs";
|
|
23
23
|
private const string TempTexturePath = "Assets/UcpImporterSmoke.png";
|
|
24
24
|
private const string TempScenePath = "Assets/UcpControllerMoveScene.unity";
|
|
25
|
+
private const string TempSceneBPath = "Assets/UcpControllerSceneB.unity";
|
|
25
26
|
private const string TempMovedFolderPath = "Assets/UcpControllerMoved";
|
|
26
27
|
private const string TempMovedAssetPath = "Assets/UcpControllerMoved/UcpControllerSmoke.asset";
|
|
27
28
|
private const string TempMovedReferenceAssetPath = "Assets/UcpControllerMoved/UcpControllerReference.asset";
|
|
28
29
|
private const string TempMovedMaterialPath = "Assets/UcpControllerMoved/UcpControllerSmoke.mat";
|
|
29
30
|
private const string TempMovedScenePath = "Assets/UcpControllerMoved/UcpControllerMoveScene.unity";
|
|
31
|
+
private const string TempReimportFolderPath = "Assets/UcpRecursiveReimport";
|
|
32
|
+
private const string TempReimportTexturePath = "Assets/UcpRecursiveReimport/UcpRecursiveA.png";
|
|
30
33
|
private const string TempProfilerExportPath = "ProfilerCaptures\\smoke-export.json";
|
|
31
34
|
private const string TempLocalPackageFolder = "TempUcpLocalPackage";
|
|
32
35
|
private const string TempLocalPackageName = "com.ucp.temp.local";
|
|
@@ -40,6 +43,8 @@ namespace UCP.Bridge.Tests
|
|
|
40
43
|
SnapshotController.Register(_router);
|
|
41
44
|
AssetController.Register(_router);
|
|
42
45
|
ImporterController.Register(_router);
|
|
46
|
+
PlayModeController.Register(_router);
|
|
47
|
+
ReferenceController.Register(_router);
|
|
43
48
|
LogsController.Register(_router);
|
|
44
49
|
HierarchyController.Register(_router);
|
|
45
50
|
ProfilerController.Register(_router);
|
|
@@ -60,7 +65,9 @@ namespace UCP.Bridge.Tests
|
|
|
60
65
|
DeleteTempScriptFile();
|
|
61
66
|
DeleteTempTextureAsset();
|
|
62
67
|
DeleteTempScene();
|
|
68
|
+
DeleteTempSceneB();
|
|
63
69
|
DeleteTempMovedFolder();
|
|
70
|
+
DeleteTempReimportFolder();
|
|
64
71
|
DeleteTempProfilerExport();
|
|
65
72
|
DeleteTempLocalPackage();
|
|
66
73
|
RemoveTempLocalPackageDependencyIfPresent();
|
|
@@ -83,7 +90,9 @@ namespace UCP.Bridge.Tests
|
|
|
83
90
|
DeleteTempScriptFile();
|
|
84
91
|
DeleteTempTextureAsset();
|
|
85
92
|
DeleteTempScene();
|
|
93
|
+
DeleteTempSceneB();
|
|
86
94
|
DeleteTempMovedFolder();
|
|
95
|
+
DeleteTempReimportFolder();
|
|
87
96
|
DeleteTempProfilerExport();
|
|
88
97
|
DeleteTempLocalPackage();
|
|
89
98
|
RemoveTempLocalPackageDependencyIfPresent();
|
|
@@ -246,6 +255,64 @@ namespace UCP.Bridge.Tests
|
|
|
246
255
|
Assert.That(Convert.ToBoolean(match["isSubAsset"]), Is.True);
|
|
247
256
|
}
|
|
248
257
|
|
|
258
|
+
[Test]
|
|
259
|
+
public void AssetSearch_SupportsRegexNameFiltering()
|
|
260
|
+
{
|
|
261
|
+
const string RegexAssetPath = "Assets/SCN_101.asset";
|
|
262
|
+
|
|
263
|
+
try
|
|
264
|
+
{
|
|
265
|
+
var sceneAsset = ScriptableObject.CreateInstance<SearchRootAsset>();
|
|
266
|
+
sceneAsset.name = "SCN_101";
|
|
267
|
+
AssetDatabase.CreateAsset(sceneAsset, RegexAssetPath);
|
|
268
|
+
AssetDatabase.SaveAssets();
|
|
269
|
+
|
|
270
|
+
var response = _router.Dispatch(
|
|
271
|
+
"asset/search",
|
|
272
|
+
1,
|
|
273
|
+
"{\"name\":\"^SCN_[0-9]+$\",\"regex\":true,\"path\":\"Assets\",\"maxResults\":10}");
|
|
274
|
+
|
|
275
|
+
Assert.That(response.error, Is.Null);
|
|
276
|
+
|
|
277
|
+
var result = (Dictionary<string, object>)response.result;
|
|
278
|
+
var matches = (List<object>)result["results"];
|
|
279
|
+
var match = FindAssetMatchByPath(matches, RegexAssetPath);
|
|
280
|
+
Assert.That(match, Is.Not.Null);
|
|
281
|
+
}
|
|
282
|
+
finally
|
|
283
|
+
{
|
|
284
|
+
if (AssetDatabase.AssetPathExists(RegexAssetPath))
|
|
285
|
+
{
|
|
286
|
+
AssetDatabase.DeleteAsset(RegexAssetPath);
|
|
287
|
+
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
[Test]
|
|
293
|
+
public void AssetSearch_DoesNotEmitSceneReadObjectThreadedErrors()
|
|
294
|
+
{
|
|
295
|
+
EditorSceneManager.NewScene(NewSceneSetup.DefaultGameObjects, NewSceneMode.Single);
|
|
296
|
+
EditorSceneManager.SaveScene(UnityEngine.SceneManagement.SceneManager.GetActiveScene(), TempScenePath);
|
|
297
|
+
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
|
298
|
+
LogsController.ClearHistoryForTests();
|
|
299
|
+
|
|
300
|
+
var response = _router.Dispatch(
|
|
301
|
+
"asset/search",
|
|
302
|
+
1,
|
|
303
|
+
"{\"name\":\"UcpControllerMoveScene\",\"path\":\"Assets\",\"maxResults\":10}");
|
|
304
|
+
|
|
305
|
+
Assert.That(response.error, Is.Null);
|
|
306
|
+
|
|
307
|
+
var status = _router.Dispatch("logs/status", 1, "{}");
|
|
308
|
+
Assert.That(status.error, Is.Null);
|
|
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));
|
|
314
|
+
}
|
|
315
|
+
|
|
249
316
|
[Test]
|
|
250
317
|
public void LogsTail_ReturnsRequestedBufferedCount()
|
|
251
318
|
{
|
|
@@ -344,6 +411,30 @@ namespace UCP.Bridge.Tests
|
|
|
344
411
|
Assert.That(result["stackTrace"], Is.EqualTo("stack line 1\nstack line 2"));
|
|
345
412
|
}
|
|
346
413
|
|
|
414
|
+
[Test]
|
|
415
|
+
public void LogsStatus_CanScopeToNewEntriesAfterCursor()
|
|
416
|
+
{
|
|
417
|
+
var first = LogsController.RecordTestLog("info", "Before");
|
|
418
|
+
LogsController.RecordTestLog("warning", "After warning");
|
|
419
|
+
LogsController.RecordTestLog("error", "After error");
|
|
420
|
+
|
|
421
|
+
var response = _router.Dispatch(
|
|
422
|
+
"logs/status",
|
|
423
|
+
1,
|
|
424
|
+
"{\"afterId\":" + Convert.ToInt64(first["id"]) + "}");
|
|
425
|
+
|
|
426
|
+
Assert.That(response.error, Is.Null);
|
|
427
|
+
|
|
428
|
+
var result = (Dictionary<string, object>)response.result;
|
|
429
|
+
Assert.That(Convert.ToInt64(result["afterId"]), Is.EqualTo(Convert.ToInt64(first["id"])));
|
|
430
|
+
Assert.That(Convert.ToInt32(result["total"]), Is.EqualTo(2));
|
|
431
|
+
|
|
432
|
+
var byLevel = (Dictionary<string, object>)result["byLevel"];
|
|
433
|
+
Assert.That(Convert.ToInt32(byLevel["info"]), Is.EqualTo(0));
|
|
434
|
+
Assert.That(Convert.ToInt32(byLevel["warning"]), Is.EqualTo(1));
|
|
435
|
+
Assert.That(Convert.ToInt32(byLevel["error"]), Is.EqualTo(1));
|
|
436
|
+
}
|
|
437
|
+
|
|
347
438
|
[Test]
|
|
348
439
|
public void ObjectLifecycle_CreateMutateAndDelete_WorksEndToEnd()
|
|
349
440
|
{
|
|
@@ -556,6 +647,28 @@ namespace UCP.Bridge.Tests
|
|
|
556
647
|
Assert.That(AssetDatabase.GetAssetPath(movedAsset.referenceAsset), Is.EqualTo(TempMovedReferenceAssetPath));
|
|
557
648
|
}
|
|
558
649
|
|
|
650
|
+
[Test]
|
|
651
|
+
public void AssetBulkMove_DryRunDoesNotMutateProject()
|
|
652
|
+
{
|
|
653
|
+
var asset = ScriptableObject.CreateInstance<SearchRootAsset>();
|
|
654
|
+
asset.name = "DryRunAsset";
|
|
655
|
+
AssetDatabase.CreateAsset(asset, TempAssetPath);
|
|
656
|
+
AssetDatabase.SaveAssets();
|
|
657
|
+
|
|
658
|
+
var response = _router.Dispatch(
|
|
659
|
+
"asset/bulk-move",
|
|
660
|
+
1,
|
|
661
|
+
"{\"dryRun\":true,\"moves\":[{\"from\":\"" + TempAssetPath + "\",\"to\":\"" + TempMovedAssetPath + "\"}]}");
|
|
662
|
+
|
|
663
|
+
Assert.That(response.error, Is.Null);
|
|
664
|
+
|
|
665
|
+
var result = (Dictionary<string, object>)response.result;
|
|
666
|
+
Assert.That(Convert.ToBoolean(result["dryRun"]), Is.True);
|
|
667
|
+
Assert.That(Convert.ToInt32(result["moved"]), Is.EqualTo(1));
|
|
668
|
+
Assert.That(AssetDatabase.LoadAssetAtPath<SearchRootAsset>(TempAssetPath), Is.Not.Null);
|
|
669
|
+
Assert.That(AssetDatabase.LoadAssetAtPath<SearchRootAsset>(TempMovedAssetPath), Is.Null);
|
|
670
|
+
}
|
|
671
|
+
|
|
559
672
|
[Test]
|
|
560
673
|
public void AssetMove_PreservesSceneMaterialReference()
|
|
561
674
|
{
|
|
@@ -758,6 +871,53 @@ namespace UCP.Bridge.Tests
|
|
|
758
871
|
Assert.That(importer.isReadable, Is.True);
|
|
759
872
|
}
|
|
760
873
|
|
|
874
|
+
[Test]
|
|
875
|
+
public void ImporterController_ReimportRecursive_ReimportsFolderContents()
|
|
876
|
+
{
|
|
877
|
+
CreateTempRecursiveTextureAsset(Color.magenta);
|
|
878
|
+
AssetImportSupport.ClearTestState();
|
|
879
|
+
|
|
880
|
+
var response = _router.Dispatch(
|
|
881
|
+
"asset/reimport",
|
|
882
|
+
1,
|
|
883
|
+
"{\"path\":\"" + TempReimportFolderPath + "\",\"recursive\":true}");
|
|
884
|
+
|
|
885
|
+
Assert.That(response.error, Is.Null);
|
|
886
|
+
|
|
887
|
+
var result = (Dictionary<string, object>)response.result;
|
|
888
|
+
Assert.That(Convert.ToBoolean(result["recursive"]), Is.True);
|
|
889
|
+
Assert.That(Convert.ToInt32(result["requested"]), Is.GreaterThanOrEqualTo(1));
|
|
890
|
+
Assert.That(Convert.ToInt32(result["reimported"]), Is.GreaterThanOrEqualTo(1));
|
|
891
|
+
Assert.That(AssetImportSupport.LastReimportedPathForTests, Is.EqualTo(TempReimportTexturePath));
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
[Test]
|
|
895
|
+
public void SceneController_Load_AdditiveKeepsExistingSceneLoaded()
|
|
896
|
+
{
|
|
897
|
+
var firstScene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
|
898
|
+
new GameObject("SceneAObject");
|
|
899
|
+
Assert.That(EditorSceneManager.SaveScene(firstScene, TempScenePath), Is.True);
|
|
900
|
+
|
|
901
|
+
var secondScene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
|
902
|
+
new GameObject("SceneBObject");
|
|
903
|
+
Assert.That(EditorSceneManager.SaveScene(secondScene, TempSceneBPath), Is.True);
|
|
904
|
+
|
|
905
|
+
EditorSceneManager.OpenScene(TempScenePath, OpenSceneMode.Single);
|
|
906
|
+
|
|
907
|
+
var response = _router.Dispatch(
|
|
908
|
+
"scene/load",
|
|
909
|
+
1,
|
|
910
|
+
"{\"path\":\"" + TempSceneBPath + "\",\"additive\":true,\"saveDirtyScenes\":true,\"discardUntitled\":true}");
|
|
911
|
+
|
|
912
|
+
Assert.That(response.error, Is.Null);
|
|
913
|
+
|
|
914
|
+
var result = (Dictionary<string, object>)response.result;
|
|
915
|
+
Assert.That(Convert.ToBoolean(result["additive"]), Is.True);
|
|
916
|
+
Assert.That(UnityEngine.SceneManagement.SceneManager.sceneCount, Is.EqualTo(2));
|
|
917
|
+
Assert.That(UnityEngine.SceneManagement.SceneManager.GetSceneByPath(TempScenePath).isLoaded, Is.True);
|
|
918
|
+
Assert.That(UnityEngine.SceneManagement.SceneManager.GetSceneByPath(TempSceneBPath).isLoaded, Is.True);
|
|
919
|
+
}
|
|
920
|
+
|
|
761
921
|
[Test]
|
|
762
922
|
public void PackagesController_DependencySetInfoAndRemove_LocalFilePackage()
|
|
763
923
|
{
|
|
@@ -1092,6 +1252,15 @@ namespace UCP.Bridge.Tests
|
|
|
1092
1252
|
}
|
|
1093
1253
|
}
|
|
1094
1254
|
|
|
1255
|
+
private static void DeleteTempSceneB()
|
|
1256
|
+
{
|
|
1257
|
+
if (AssetDatabase.LoadMainAssetAtPath(TempSceneBPath) != null)
|
|
1258
|
+
{
|
|
1259
|
+
AssetDatabase.DeleteAsset(TempSceneBPath);
|
|
1260
|
+
AssetDatabase.SaveAssets();
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1095
1264
|
private static void DeleteTempMovedFolder()
|
|
1096
1265
|
{
|
|
1097
1266
|
if (AssetDatabase.IsValidFolder(TempMovedFolderPath))
|
|
@@ -1101,6 +1270,15 @@ namespace UCP.Bridge.Tests
|
|
|
1101
1270
|
}
|
|
1102
1271
|
}
|
|
1103
1272
|
|
|
1273
|
+
private static void DeleteTempReimportFolder()
|
|
1274
|
+
{
|
|
1275
|
+
if (AssetDatabase.IsValidFolder(TempReimportFolderPath))
|
|
1276
|
+
{
|
|
1277
|
+
AssetDatabase.DeleteAsset(TempReimportFolderPath);
|
|
1278
|
+
AssetDatabase.SaveAssets();
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1104
1282
|
private static void DeleteTempProfilerExport()
|
|
1105
1283
|
{
|
|
1106
1284
|
var fullPath = ResolveProjectRelativePath(TempProfilerExportPath);
|
|
@@ -1180,6 +1358,24 @@ namespace UCP.Bridge.Tests
|
|
|
1180
1358
|
ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport);
|
|
1181
1359
|
}
|
|
1182
1360
|
|
|
1361
|
+
private static void CreateTempRecursiveTextureAsset(Color color)
|
|
1362
|
+
{
|
|
1363
|
+
DeleteTempReimportFolder();
|
|
1364
|
+
Directory.CreateDirectory(ResolveProjectRelativePath(TempReimportFolderPath));
|
|
1365
|
+
|
|
1366
|
+
var texture = new Texture2D(2, 2, TextureFormat.RGBA32, false);
|
|
1367
|
+
texture.SetPixels(new[] { color, color, color, color });
|
|
1368
|
+
texture.Apply();
|
|
1369
|
+
|
|
1370
|
+
var bytes = texture.EncodeToPNG();
|
|
1371
|
+
UnityEngine.Object.DestroyImmediate(texture);
|
|
1372
|
+
|
|
1373
|
+
File.WriteAllBytes(ResolveProjectRelativePath(TempReimportTexturePath), bytes);
|
|
1374
|
+
AssetDatabase.ImportAsset(
|
|
1375
|
+
TempReimportTexturePath,
|
|
1376
|
+
ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1183
1379
|
private static string ResolveProjectRelativePath(string assetPath)
|
|
1184
1380
|
{
|
|
1185
1381
|
var projectRoot = Path.GetDirectoryName(Application.dataPath);
|
|
@@ -1228,6 +1424,19 @@ namespace UCP.Bridge.Tests
|
|
|
1228
1424
|
return null;
|
|
1229
1425
|
}
|
|
1230
1426
|
|
|
1427
|
+
private static Dictionary<string, object> FindAssetMatchByPath(List<object> matches, string expectedPath)
|
|
1428
|
+
{
|
|
1429
|
+
foreach (var entry in matches)
|
|
1430
|
+
{
|
|
1431
|
+
var match = (Dictionary<string, object>)entry;
|
|
1432
|
+
var path = match.ContainsKey("path") ? match["path"].ToString() : string.Empty;
|
|
1433
|
+
if (path == expectedPath)
|
|
1434
|
+
return match;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
return null;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1231
1440
|
private sealed class SearchRootAsset : ScriptableObject
|
|
1232
1441
|
{
|
|
1233
1442
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mflrevan/ucp",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Unity Control Protocol - CLI for programmatic Unity Editor control",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/mflRevan/unity-control-protocol.git"
|
|
8
|
+
"url": "git+https://github.com/mflRevan/unity-control-protocol.git"
|
|
9
9
|
},
|
|
10
10
|
"bin": {
|
|
11
11
|
"ucp": "bin/ucp.js"
|