Skip to content
Merged
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
1 change: 1 addition & 0 deletions Assets/Tests/Editor/WatchResponseContractTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public void WatchResponse_WhenSerialized_MatchesSharedContractFieldShape()
Expression = "1 + 2",
MaxHistory = 20,
HistoryDroppedCount = 0,
ValueFrozenHint = "",
History = new List<WatchHistoryResponse>
{
new()
Expand Down
107 changes: 107 additions & 0 deletions Assets/Tests/Editor/WatchValueFreezeHintEvaluatorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using System.Collections.Generic;

using NUnit.Framework;

using io.github.hatayama.UnityCliLoop.FirstPartyTools;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor
{
/// <summary>
/// Verifies WatchValueFreezeHintEvaluator's detection of a watch value that looks frozen.
/// </summary>
[TestFixture]
public sealed class WatchValueFreezeHintEvaluatorTests
{
[Test]
public void EvaluateFreezeHint_WhenHistoryHasFewerEntriesThanTheThreshold_ReturnsEmpty()
{
// Tests that a short history (not enough evaluations to judge staleness) never hints.
List<WatchHistoryResponse> history = CreateHistory("1", "1");

string hint = WatchValueFreezeHintEvaluator.EvaluateFreezeHint(history);

Assert.That(hint, Is.Empty);
}

[Test]
public void EvaluateFreezeHint_WhenRecentValuesAreIdentical_ReturnsHint()
{
// Tests the core case: the last several evaluations returned the same value.
List<WatchHistoryResponse> history = CreateHistory("1", "1", "1");

string hint = WatchValueFreezeHintEvaluator.EvaluateFreezeHint(history);

Assert.That(hint, Is.Not.Empty);
Assert.That(hint, Does.Contain("pause point"));
}

[Test]
public void EvaluateFreezeHint_WhenRecentValuesDiffer_ReturnsEmpty()
{
// Tests that a genuinely changing value is never flagged as frozen.
List<WatchHistoryResponse> history = CreateHistory("1", "2", "3");

string hint = WatchValueFreezeHintEvaluator.EvaluateFreezeHint(history);

Assert.That(hint, Is.Empty);
}

[Test]
public void EvaluateFreezeHint_WhenOnlyAnOlderEntryDiffers_ReturnsHintFromRecentWindowOnly()
{
// Tests that the check looks at the trailing window, not the whole history: an old
// change that happened before the most recent repeats must not suppress the hint.
List<WatchHistoryResponse> history = CreateHistory("0", "1", "1", "1");

string hint = WatchValueFreezeHintEvaluator.EvaluateFreezeHint(history);

Assert.That(hint, Is.Not.Empty);
}

[Test]
public void EvaluateFreezeHint_WhenARecentEvaluationFailed_ReturnsEmpty()
{
// Tests that evaluation errors are not mistaken for a frozen value; a failure is a
// distinct problem the freeze hint should not paper over.
List<WatchHistoryResponse> history = new()
{
CreateEntry("1", success: true),
CreateEntry("1", success: true),
CreateEntry(string.Empty, success: false)
};

string hint = WatchValueFreezeHintEvaluator.EvaluateFreezeHint(history);

Assert.That(hint, Is.Empty);
}

[Test]
public void EvaluateFreezeHint_WhenHistoryIsNull_ReturnsEmpty()
{
// Tests the defensive null-history path used before any evaluation has occurred.
string hint = WatchValueFreezeHintEvaluator.EvaluateFreezeHint(null);

Assert.That(hint, Is.Empty);
}

private static List<WatchHistoryResponse> CreateHistory(params string[] values)
{
List<WatchHistoryResponse> history = new();
foreach (string value in values)
{
history.Add(CreateEntry(value, success: true));
}

return history;
}

private static WatchHistoryResponse CreateEntry(string value, bool success)
{
return new WatchHistoryResponse
{
Success = success,
Value = value
};
}
}
}
11 changes: 11 additions & 0 deletions Assets/Tests/Editor/WatchValueFreezeHintEvaluatorTests.cs.meta

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

14 changes: 11 additions & 3 deletions Packages/src/Editor/FirstPartyTools/Watch/WatchTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,25 @@ public sealed class WatchEntryResponse
public IReadOnlyList<WatchHistoryResponse> History { get; set; } =
Array.Empty<WatchHistoryResponse>();

/// <summary>
/// Non-empty when recent evaluations returned the same value, suggesting the linked
/// pause point has not been hit again since watches only refresh on a changed, paused frame.
/// </summary>
public string ValueFrozenHint { get; set; } = string.Empty;

internal static WatchEntryResponse FromEntry(WatchExpressionEntry entry)
{
List<WatchHistoryResponse> history = entry.CreateHistorySnapshot()
.Select(WatchHistoryResponse.FromEntry)
.ToList();
return new WatchEntryResponse
{
Id = entry.Id,
Expression = entry.Expression,
MaxHistory = entry.MaxHistory,
HistoryDroppedCount = entry.HistoryDroppedCount,
History = entry.CreateHistorySnapshot()
.Select(WatchHistoryResponse.FromEntry)
.ToList()
History = history,
ValueFrozenHint = WatchValueFreezeHintEvaluator.EvaluateFreezeHint(history)
};
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Collections.Generic;
using System.Linq;

namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
{
/// <summary>
/// Detects a watch expression whose recent evaluations returned the same value, so
/// get-watch-values can explain that watches only refresh on a changed, paused frame -
/// a value that looks stuck usually means the linked pause point has not been hit again.
/// </summary>
internal static class WatchValueFreezeHintEvaluator
{
internal const int MinIdenticalEvaluationsForHint = 3;

private const string FreezeHintMessageFormat =
"Value has not changed across the last {0} evaluations. Watch values only refresh " +
"when the Editor is paused on a changed frame, so confirm the linked pause point has " +
"been hit again if you expect this value to be different (a marker on a conditional " +
"line freezes after its first hit).";

public static string EvaluateFreezeHint(IReadOnlyList<WatchHistoryResponse> history)
{
if (history == null || history.Count < MinIdenticalEvaluationsForHint)
{
return string.Empty;
}

List<WatchHistoryResponse> recent = history
.Skip(history.Count - MinIdenticalEvaluationsForHint)
.ToList();
if (recent.Any(entry => !entry.Success))
{
return string.Empty;
}

string firstValue = recent[0].Value;
bool allIdentical = recent.All(entry => entry.Value == firstValue);
return allIdentical
? string.Format(FreezeHintMessageFormat, MinIdenticalEvaluationsForHint)
: string.Empty;
}
}
}

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

1 change: 1 addition & 0 deletions cli/project-runner/internal/projectrunner/watch_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type watchEntryResponse struct {
Expression string `json:"Expression"`
MaxHistory int `json:"MaxHistory"`
HistoryDroppedCount int `json:"HistoryDroppedCount"`
ValueFrozenHint string `json:"ValueFrozenHint"`
History []watchHistoryResponse `json:"History"`
}

Expand Down
1 change: 1 addition & 0 deletions tests/contracts/watch_response_contract.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"Expression": "1 + 2",
"MaxHistory": 20,
"HistoryDroppedCount": 0,
"ValueFrozenHint": "",
"History": [
{
"FrameCount": 42,
Expand Down