Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using UnityEditor;
using UnityEngine;
using NUnit.Framework;
using System.Collections;
using UnityEngine.TestTools;

namespace Aspid.FastTools.UIElements.Editors.Internal.Tests
{
/// <summary>
/// Behavioural coverage for the dots canvas' status wash: a status class must repaint all three blobs one flat
/// tone through USS, and dropping it must bring the default three-tone signal gradient back.
/// </summary>
/// <remarks>
/// The round trip is the point. The wash used to be applied as inline colors, which latched the blobs out of USS
/// resolution for good and forced the "restore the gradient" path to re-supply the palette values from C#.
/// Asserted structurally (blobs equal / distinct, channel ordering) rather than against literal rgb values —
/// pinning the numbers here would re-create exactly the palette duplication this covers.
/// </remarks>
[TestFixture]
internal sealed class AspidAnimatedDotsBackgroundStatusTests
{
private EditorWindow _window;
private AspidAnimatedDotsBackground _canvas;

[SetUp]
public void SetUp()
{
_window = ScriptableObject.CreateInstance<EditorWindow>();
_window.ShowUtility();

// The default gradient resolves through the shared palette, so the host needs the theme sheets — the
// status washes themselves are component-scoped and come with the canvas' own stylesheet.
_window.rootVisualElement.AddAspidThemeStyleSheets();

_canvas = new AspidAnimatedDotsBackground();
_window.rootVisualElement.Add(_canvas);
}

[TearDown]
public void TearDown()
{
if (_window) Object.DestroyImmediate(_window);
}

[UnityTest]
public IEnumerator None_ResolvesTheThreeToneSignalGradient()
{
yield return null;

Assert.AreNotEqual(_canvas.Color1, _canvas.Color2, "The default canvas must keep its three distinct signal blobs.");
Assert.AreNotEqual(_canvas.Color2, _canvas.Color3, "The default canvas must keep its three distinct signal blobs.");
}

[UnityTest]
public IEnumerator Status_PaintsEveryBlobTheSameWash()
{
_canvas.Status = StatusStyle.Type.Warning;
yield return null;

Assert.AreEqual(_canvas.Color1, _canvas.Color2, "A status wash must paint every blob the one tone.");
Assert.AreEqual(_canvas.Color2, _canvas.Color3, "A status wash must paint every blob the one tone.");

var wash = _canvas.Color1;
Assert.Greater(wash.r, wash.g, "The warning wash must read amber — red over green over blue.");
Assert.Greater(wash.g, wash.b, "The warning wash must read amber — red over green over blue.");
}

[UnityTest]
public IEnumerator Status_SwapsBetweenWashes()
{
_canvas.Status = StatusStyle.Type.Warning;
yield return null;

var warning = _canvas.Color1;

_canvas.Status = StatusStyle.Type.Success;
yield return null;

Assert.AreNotEqual(warning, _canvas.Color1, "Switching status must repaint the wash.");
Assert.Greater(_canvas.Color1.g, _canvas.Color1.r, "The success wash must read green.");
}

[UnityTest]
public IEnumerator None_RestoresTheGradientAfterAWash()
{
_canvas.Status = StatusStyle.Type.Warning;
yield return null;

Assert.AreEqual(_canvas.Color1, _canvas.Color2, "Precondition: the wash is on.");

_canvas.Status = StatusStyle.Type.None;
yield return null;

Assert.AreNotEqual(_canvas.Color1, _canvas.Color2, "Dropping the status must hand the blobs back to the USS gradient.");
Assert.AreNotEqual(_canvas.Color2, _canvas.Color3, "Dropping the status must hand the blobs back to the USS gradient.");
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,42 @@
--aspid-fasttools-colors-dot_blob-color_2: var(--aspid-colors-status-warning-text-dark);
--aspid-fasttools-colors-dot_blob-color_3: var(--aspid-colors-status-error-text-dark);

/* Status washes: dim, desaturated hues a status class paints across all three blobs at once, so the canvas
reads as one calm state wash instead of the default good→bad gradient. Component-scoped — the palette's
status families are tuned for text and borders and read far too saturated behind a whole window. */
--aspid-fasttools-colors-dot_blob-info: rgb(38, 54, 77);
--aspid-fasttools-colors-dot_blob-success: rgb(41, 71, 54);
--aspid-fasttools-colors-dot_blob-warning: rgb(77, 64, 31);
--aspid-fasttools-colors-dot_blob-error: rgb(82, 41, 41);

--aspid-fasttools-metrics-dot_radius: 1.55;
--aspid-fasttools-metrics-dot_spacing: 18;
--aspid-fasttools-metrics-dot_scale_reference: 420;
}

/* Declared after :root deliberately — a status class and :root carry the same specificity, so source order is what
lets a status win over the default gradient. Dropping the class falls back to :root, which is how StatusStyle.Type.None
restores the three-tone signal look. */
.aspid-fasttools-status--info {
--aspid-fasttools-colors-dot_blob-color_1: var(--aspid-fasttools-colors-dot_blob-info);
--aspid-fasttools-colors-dot_blob-color_2: var(--aspid-fasttools-colors-dot_blob-info);
--aspid-fasttools-colors-dot_blob-color_3: var(--aspid-fasttools-colors-dot_blob-info);
}

.aspid-fasttools-status--success {
--aspid-fasttools-colors-dot_blob-color_1: var(--aspid-fasttools-colors-dot_blob-success);
--aspid-fasttools-colors-dot_blob-color_2: var(--aspid-fasttools-colors-dot_blob-success);
--aspid-fasttools-colors-dot_blob-color_3: var(--aspid-fasttools-colors-dot_blob-success);
}

.aspid-fasttools-status--warning {
--aspid-fasttools-colors-dot_blob-color_1: var(--aspid-fasttools-colors-dot_blob-warning);
--aspid-fasttools-colors-dot_blob-color_2: var(--aspid-fasttools-colors-dot_blob-warning);
--aspid-fasttools-colors-dot_blob-color_3: var(--aspid-fasttools-colors-dot_blob-warning);
}

.aspid-fasttools-status--error {
--aspid-fasttools-colors-dot_blob-color_1: var(--aspid-fasttools-colors-dot_blob-error);
--aspid-fasttools-colors-dot_blob-color_2: var(--aspid-fasttools-colors-dot_blob-error);
--aspid-fasttools-colors-dot_blob-color_3: var(--aspid-fasttools-colors-dot_blob-error);
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
using System;
using UnityEditor;
using System.Linq;
using System.Collections.Generic;

// ReSharper disable once CheckNamespace
namespace Aspid.FastTools.SerializeReferences.Editors
{
/// <summary>One broken managed-reference entry plus the asset it lives in.</summary>
internal readonly struct MissingReferenceLocation
{
public readonly string AssetPath;
public readonly MissingReferenceEntry Entry;

public MissingReferenceLocation(string assetPath, MissingReferenceEntry entry)
{
AssetPath = assetPath;
Entry = entry;
}
}

/// <summary>
/// Every broken reference sharing one stored type across the project — the unit the Project References audit lists
/// and bulk-fixes. Resolves a single picker constraint by intersecting the entries' declared field types, falling
/// back to <see cref="object"/> when they disagree.
/// </summary>
internal sealed class MissingReferenceGroup
{
public readonly ManagedTypeName StoredType;
public readonly List<MissingReferenceLocation> Entries = new();

private readonly HashSet<string> _files = new(StringComparer.Ordinal);
private readonly SerializeReferenceConstraintCache _constraints = new();

public MissingReferenceGroup(ManagedTypeName storedType)
{
StoredType = storedType;
}

public int FileCount => _files.Count;

public string DisplayName => StoredType.DisplayName;

/// <summary>
/// Groups every unresolved managed reference in the project by stored type, backed by the shared usage index,
/// biggest group first. Cheap once the index is warm — it is an in-memory filter, not a sweep.
/// </summary>
public static List<MissingReferenceGroup> CollectFromIndex()
{
var byType = new Dictionary<string, MissingReferenceGroup>(StringComparer.Ordinal);

foreach (var usage in SerializeReferenceTypeUsageIndex.EnumerateUnresolved())
{
var path = AssetDatabase.GUIDToAssetPath(usage.Guid);
if (string.IsNullOrEmpty(path)) continue;

var key = SerializeReferenceHelpers.StoredTypeKey(usage.StoredType);
if (!byType.TryGetValue(key, out var group))
{
group = new MissingReferenceGroup(usage.StoredType);
byType.Add(key, group);
}

group.Add(path, new MissingReferenceEntry(usage.FileId, usage.Rid, usage.StoredType));
}

var groups = byType.Values.ToList();
groups.Sort((a, b) => b.Entries.Count.CompareTo(a.Entries.Count));
return groups;
}

public void Add(string assetPath, MissingReferenceEntry entry)
{
Entries.Add(new MissingReferenceLocation(assetPath, entry));
_files.Add(assetPath);
}

/// <summary>
/// The ranked Smart Fix for this group's broken type, or <see langword="false"/> when nothing clears the
/// confidence threshold.
/// </summary>
/// <remarks>
/// Ranked against the constraint-filtered pool, so the suggestion is always assignable — which is what lets a
/// quick-apply bypass the picker. The field names come from the first entry: every entry in a group stores the
/// same broken type, so any of them ranks the same candidates.
/// </remarks>
public bool TryGetSuggestion(Type constraint, out SerializeReferenceRepairSuggestions.RepairCandidate suggestion)
{
suggestion = default;

var first = Entries[0];
var fieldNames = SerializeReferenceYamlEditor.GetReferenceFieldNames(first.AssetPath, first.Entry.FileId, first.Entry.Rid);

var ranked = SerializeReferenceRepairSuggestions.Rank(StoredType, fieldNames, constraint);
if (ranked.Count == 0) return false;

suggestion = ranked[0];
return true;
}

/// <summary>The type every entry's field can hold, or <see cref="object"/> when that cannot be narrowed.</summary>
/// <remarks>Per-file constraint maps are built once and cached, so the intersection costs one scan per distinct asset.</remarks>
public Type ResolveConstraint() => ResolveConstraint(out _);

/// <inheritdoc cref="ResolveConstraint()"/>
/// <param name="mixedFieldTypes">
/// Whether the <see cref="object"/> fallback came from the field types disagreeing (vs. one being
/// unrecoverable) — the bulk-fix confirmation warns on that case.
/// </param>
public Type ResolveConstraint(out bool mixedFieldTypes)
{
mixedFieldTypes = false;
Type common = null;

foreach (var entry in Entries)
{
// A field type we cannot recover (a reference nested in a missing parent, or an orphaned rid no
// field points at) leaves the group unconstrained — a tighter guess could hide a valid pick.
var fieldType = _constraints.Resolve(entry.AssetPath, entry.Entry.FileId, entry.Entry.Rid);
if (fieldType is null) return typeof(object);

if (common is null)
{
common = fieldType;
}
else if (common != fieldType)
{
mixedFieldTypes = true;
return typeof(object);
}
}

return common ?? typeof(object);
}
}

/// <summary>
/// A group's picker constraint and whether it reads as a one-click <c>[MovedFrom]</c> migration, resolved once so
/// the audit's partition, card body and picker label share one computation and can never disagree.
/// </summary>
/// <remarks>
/// A migration is an authoritative <c>[MovedFrom]</c> rename whose target also fits the group's field constraint —
/// <c>Migrate all</c> bypasses the picker's assignability guarantee, and an incompatible target would be nulled by
/// Unity at load, so the constraint gate matters.
/// </remarks>
internal readonly struct MissingReferenceMigration
{
public readonly Type Constraint;
public readonly bool IsMigration;

/// <summary>The <c>[MovedFrom]</c> target when <see cref="IsMigration"/>; otherwise <see langword="null"/>.</summary>
public readonly Type Target;

public MissingReferenceMigration(MissingReferenceGroup group)
{
Constraint = group.ResolveConstraint();
IsMigration = SerializeReferenceMovedFromResolver.TryResolve(group.StoredType, out var target) &&
(Constraint == typeof(object) || Constraint.IsAssignableFrom(target));
Target = IsMigration ? target : null;
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading