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
13 changes: 13 additions & 0 deletions Assets/Tests/Editor/PausePointPerFrameTraceNoticeFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace io.github.hatayama.UnityCliLoop.Tests.PausePointToolsFixtures
{
internal sealed class PerFrameTraceNoticeFixture
{
private int _probe;

public void Update()
{
// per-frame-trace-notice-probe-unique
_probe = 1;
}
}
}
11 changes: 11 additions & 0 deletions Assets/Tests/Editor/PausePointPerFrameTraceNoticeFixture.cs.meta

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

193 changes: 193 additions & 0 deletions Assets/Tests/Editor/PausePointPerFrameTraceNoticeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
using System;
using System.IO;

using NUnit.Framework;

using io.github.hatayama.UnityCliLoop.FirstPartyTools;
using io.github.hatayama.UnityCliLoop.Runtime;
using io.github.hatayama.UnityCliLoop.ToolContracts;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor
{
/// <summary>
/// Verifies the enable-time notice when a trace marker's method name matches a per-frame Unity message.
/// </summary>
[TestFixture]
public sealed class PausePointPerFrameTraceNoticeTests
{
private const string FixtureFilePath = "Assets/Tests/Editor/PausePointPerFrameTraceNoticeFixture.cs";

private const string PlayerUpdateNotice =
"'Player.Update' matches a per-frame Unity message name; if this line runs every frame, capture mode 'trace' can roll the history (max 8) over within moments. Prefer a conditional line or a larger --max-history.";

[SetUp]
public void SetUp()
{
UloopPausePointRegistry.ConfigureForTests(new FakePausePointPauseController(), () => DateTime.UtcNow);
}

[TearDown]
public void TearDown()
{
SourcePausePointPatcher.UnpatchAll();
UloopPausePointRegistry.ResetForTests();
}

/// <summary>
/// What: trace on Update / FixedUpdate / LateUpdate / OnGUI appends the per-frame notice.
/// </summary>
[TestCase(
"Player.Update",
"'Player.Update' matches a per-frame Unity message name; if this line runs every frame, capture mode 'trace' can roll the history (max 8) over within moments. Prefer a conditional line or a larger --max-history.")]
[TestCase(
"Player.FixedUpdate",
"'Player.FixedUpdate' matches a per-frame Unity message name; if this line runs every frame, capture mode 'trace' can roll the history (max 8) over within moments. Prefer a conditional line or a larger --max-history.")]
[TestCase(
"Player.LateUpdate",
"'Player.LateUpdate' matches a per-frame Unity message name; if this line runs every frame, capture mode 'trace' can roll the history (max 8) over within moments. Prefer a conditional line or a larger --max-history.")]
[TestCase(
"Player.OnGUI",
"'Player.OnGUI' matches a per-frame Unity message name; if this line runs every frame, capture mode 'trace' can roll the history (max 8) over within moments. Prefer a conditional line or a larger --max-history.")]
public void BuildPerFrameTraceWarningOrEmpty_WhenTraceAndPerFrameMessage_ReturnsNotice(
string resolvedMethod,
string expectedNotice)
{
string warning = PausePointEnableWarnings.BuildPerFrameTraceWarningOrEmpty(
UloopPausePointCaptureMode.Trace,
resolvedMethod,
8);

Assert.That(warning, Is.EqualTo(expectedNotice));
}

/// <summary>
/// What: a Cecil FullName resolved method still interpolates Type.Method into the notice.
/// </summary>
[Test]
public void BuildPerFrameTraceWarningOrEmpty_WhenResolvedMethodIsCecilFullName_FormatsTypeMethod()
{
string warning = PausePointEnableWarnings.BuildPerFrameTraceWarningOrEmpty(
UloopPausePointCaptureMode.Trace,
"System.Void Ns.Player::Update()",
8);

Assert.That(warning, Is.EqualTo(PlayerUpdateNotice));
}

/// <summary>
/// What: non-trace capture modes stay silent even when the method name matches a per-frame Unity message.
/// </summary>
[TestCase(UloopPausePointCaptureMode.SingleShot)]
[TestCase(UloopPausePointCaptureMode.Continuous)]
public void BuildPerFrameTraceWarningOrEmpty_WhenModeIsNotTrace_ReturnsEmpty(string captureMode)
{
string warning = PausePointEnableWarnings.BuildPerFrameTraceWarningOrEmpty(
captureMode,
"Player.Update",
8);

Assert.That(warning, Is.EqualTo(string.Empty));
}

/// <summary>
/// What: a method whose simple name is not a per-frame Unity message stays silent in trace.
/// </summary>
[TestCase("Player.Start")]
[TestCase("Player.UpdateWeather")]
[TestCase("Player.OnGUILayout")]
public void BuildPerFrameTraceWarningOrEmpty_WhenSimpleNameIsNotPerFrame_ReturnsEmpty(string resolvedMethod)
{
string warning = PausePointEnableWarnings.BuildPerFrameTraceWarningOrEmpty(
UloopPausePointCaptureMode.Trace,
resolvedMethod,
8);

Assert.That(warning, Is.EqualTo(string.Empty));
}

/// <summary>
/// What: the per-frame notice is appended after an existing enable warning.
/// </summary>
[Test]
public void MergeWarnings_WhenPriorWarningAndPerFrameNotice_AppendsNoticeAfterPrior()
{
string prior = "Pause point was enabled before PlayMode while Domain Reload is enabled. Entering PlayMode may clear this marker; keep Domain Reload disabled for this workflow or enable the marker after PlayMode starts.";
string notice = PausePointEnableWarnings.BuildPerFrameTraceWarningOrEmpty(
UloopPausePointCaptureMode.Trace,
"Player.Update",
8);

string merged = PausePointEnableWarnings.MergeWarnings(prior, notice);

Assert.That(
merged,
Is.EqualTo(
"Pause point was enabled before PlayMode while Domain Reload is enabled. Entering PlayMode may clear this marker; keep Domain Reload disabled for this workflow or enable the marker after PlayMode starts. 'Player.Update' matches a per-frame Unity message name; if this line runs every frame, capture mode 'trace' can roll the history (max 8) over within moments. Prefer a conditional line or a larger --max-history."));
}

/// <summary>
/// What: enable with file:line trace on Update appends the per-frame notice using the effective max-history.
/// </summary>
[Test]
public void Enable_WhenTraceMarkerIsOnUpdate_AppendsPerFrameNotice()
{
string absolutePath = Path.Combine(UnityCliLoopPathResolver.GetProjectRoot(), FixtureFilePath);
string diskSource = File.ReadAllText(absolutePath);
int requestedLine = FindLineNumberContaining(diskSource, "per-frame-trace-notice" + "-probe-unique") + 1;
Assert.That(requestedLine, Is.GreaterThan(1));

PausePointResponse response = new PausePointUseCase().Enable(new EnablePausePointSchema
{
File = FixtureFilePath,
Line = requestedLine,
TimeoutSeconds = 30,
Mode = UloopPausePointCaptureMode.Trace,
MaxHistory = 8
});

Assert.That(
response.Success,
Is.True,
response.ErrorCode + " / " + response.Message + " / " + response.RecommendedNextAction);
string notice =
"'PerFrameTraceNoticeFixture.Update' matches a per-frame Unity message name; if this line runs every frame, capture mode 'trace' can roll the history (max 8) over within moments. Prefer a conditional line or a larger --max-history.";
string expectedWarning = PausePointEnableWarnings.MergeWarnings(
PausePointEnableWarnings.MergeWarnings(
PausePointEnableWarnings.CreateEnableWarning(),
SourcePausePointConstants.SmallMethodInliningRiskWarning),
notice);
Assert.That(response.Warning, Is.EqualTo(expectedWarning));
}

private static int FindLineNumberContaining(string source, string fragment)
{
string[] lines = source.Replace("\r\n", "\n").Split('\n');
for (int index = 0; index < lines.Length; index++)
{
if (lines[index].Contains(fragment))
{
return index + 1;
}
}

return -1;
}

private sealed class FakePausePointPauseController : IUloopPausePointPauseController
{
public int PauseCount { get; private set; }
public bool IsPlaying => true;
public bool IsPaused => PauseCount > 0;

public void Pause()
{
PauseCount++;
}

public void Resume()
{
PauseCount = 0;
}
}
}
}
11 changes: 11 additions & 0 deletions Assets/Tests/Editor/PausePointPerFrameTraceNoticeTests.cs.meta

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 @@ -65,6 +65,80 @@ internal static string MergeWarnings(string first, string second)
return first + " " + second;
}

// Why Type.Method in the notice: enable's resolved-method string is Cecil FullName, but
// the warning should name the Unity message the same way agents already read caller frames.
internal static string BuildPerFrameTraceWarningOrEmpty(
string captureMode,
string resolvedMethod,
int maxHistory)
{
if (captureMode != UloopPausePointCaptureMode.Trace || string.IsNullOrEmpty(resolvedMethod))
{
return string.Empty;
}

string simpleName = ExtractSimpleMethodName(resolvedMethod);
if (!IsPerFrameUnityMessageSimpleName(simpleName))
{
return string.Empty;
}

return string.Format(
SourcePausePointConstants.PerFrameTraceNoticeFormat,
FormatTypeMethodDisplay(resolvedMethod, simpleName),
maxHistory);
}

private static bool IsPerFrameUnityMessageSimpleName(string simpleName)
{
return simpleName == "Update"
|| simpleName == "FixedUpdate"
|| simpleName == "LateUpdate"
|| simpleName == "OnGUI";
}

private static string ExtractSimpleMethodName(string resolvedMethod)
{
int colon = resolvedMethod.IndexOf("::", StringComparison.Ordinal);
if (colon >= 0)
{
int start = colon + 2;
int paren = resolvedMethod.IndexOf('(', start);
if (paren >= 0)
{
return resolvedMethod.Substring(start, paren - start);
}

return resolvedMethod.Substring(start);
}

int lastDot = resolvedMethod.LastIndexOf('.');
string tail = lastDot >= 0 ? resolvedMethod.Substring(lastDot + 1) : resolvedMethod;
int tailParen = tail.IndexOf('(');
if (tailParen >= 0)
{
return tail.Substring(0, tailParen);
}

return tail;
}

private static string FormatTypeMethodDisplay(string resolvedMethod, string simpleName)
{
int colon = resolvedMethod.IndexOf("::", StringComparison.Ordinal);
if (colon < 0)
{
return resolvedMethod;
}

string beforeColon = resolvedMethod.Substring(0, colon);
int space = beforeColon.LastIndexOf(' ');
string typeFullName = space >= 0 ? beforeColon.Substring(space + 1) : beforeColon;
int typeSep = Math.Max(typeFullName.LastIndexOf('.'), typeFullName.LastIndexOf('/'));
string typeName = typeSep >= 0 ? typeFullName.Substring(typeSep + 1) : typeFullName;
return typeName + "." + simpleName;
}

// Why success-only: resolve failure leaves ResolvedMethod and ResolvedLineText empty,
// so this wording would point at fields that are not on the response.
// Why same resolvedLine on both sides: the resolver rounds empty/comment lines forward,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,12 @@ private static PausePointResponse FinishEnableBySourceLocation(
response.Warning = PausePointEnableWarnings.MergeWarnings(
response.Warning,
PausePointEnableWarnings.BuildAddedFieldsNotCapturedWarningOrEmpty(patchResult.DeclaringType));
response.Warning = PausePointEnableWarnings.MergeWarnings(
response.Warning,
PausePointEnableWarnings.BuildPerFrameTraceWarningOrEmpty(
parameters.Mode,
resolvedMethod,
snapshot.MaxHistory));
LogEnable(response.Id, response.ResolvedMethod, $"{parameters.File}:{response.ResolvedLine}", response.Mode, response.Warning);

if (patchResult.HasPhysicsCallbackWarning)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,14 @@ internal static class SourcePausePointConstants
"The target method body is very small and may be inlined by Mono's JIT into its callers; "
+ "if HitCount stays 0 while the line demonstrably runs, move the pause point into the calling method.";

// Format: Type.Method display name, effective max-history.
// Why name-based, not a MonoBehaviour type check: a plain C# Update is often driven
// every frame by a MonoBehaviour delegate, and a type check would miss that case.
// Why conditional wording: the name match is not proof this is a Unity message, so
// "is a per-frame Unity message" would be false for non-MonoBehaviour types.
public const string PerFrameTraceNoticeFormat =
"'{0}' matches a per-frame Unity message name; if this line runs every frame, capture mode 'trace' can roll the history (max {1}) over within moments. Prefer a conditional line or a larger --max-history.";

// Callers have observed captured values that look like they belong to the line after
// ResolvedLine; this makes the pre-line snapshot timing explicit in the response itself
// instead of leaving it documented only in the skill.
Expand Down
Loading