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
144 changes: 144 additions & 0 deletions Assets/Tests/Editor/PausePointCompiledLineMapWarningTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ public void Enable_WhenCompiledLineDriftsFromEditedFile_AddsDriftWarningAndNextA

string[] snapshotLines = diskSource.Replace("\r\n", "\n").Split('\n');
snapshotLines[requestedLine - 1] = " return 0;";
snapshotLines[markerLine - 1] = " return 424242;";
string snapshotSource = string.Join("\n", snapshotLines);

try
Expand Down Expand Up @@ -259,6 +260,10 @@ public void Enable_WhenCompiledLineDriftsFromEditedFile_AddsDriftWarningAndNextA
response.ResolvedMethod,
spanResult.Resolution.CompiledMethodStartLine,
spanResult.Resolution.CompiledMethodEndLine);
expectedDrift = PausePointEnableWarnings.AppendCandidateCompiledLinesToDriftWarningOrUnchanged(
expectedDrift,
"return 424242;",
snapshotLines);
string expectedWarning = PausePointEnableWarnings.MergeWarnings(
PausePointEnableWarnings.MergeWarnings(
PausePointEnableWarnings.MergeWarnings(
Expand Down Expand Up @@ -344,6 +349,145 @@ public void AppendCompiledMethodSpanToDriftWarningOrUnchanged_WhenDriftIsEmpty_R
Assert.That(warning, Is.EqualTo(string.Empty));
}

/// <summary>
/// What: one trimmed compiled-source match appends a single-line candidate to the drift warning.
/// </summary>
[Test]
public void AppendCandidateCompiledLinesToDriftWarningOrUnchanged_WhenOneLineMatches_AppendsSingleCandidate()
{
string drift = string.Format(
SourcePausePointConstants.HotReloadCompiledLineMapLineDriftWarningFormat,
ForwardSlashFile,
17,
"return 1;",
"return 2;");
string[] compiledLines =
{
"class Sample",
" return 2;",
" return 1;"
};

string warning = PausePointEnableWarnings.AppendCandidateCompiledLinesToDriftWarningOrUnchanged(
drift,
" return 2; ",
compiledLines);

Assert.That(
warning,
Is.EqualTo(
drift + " Candidate: the edited line's text appears at line 2 in the last compiled source."));
}

/// <summary>
/// What: two or three trimmed compiled-source matches append every matching line number.
/// </summary>
[Test]
public void AppendCandidateCompiledLinesToDriftWarningOrUnchanged_WhenThreeLinesMatch_AppendsAllCandidates()
{
string drift = string.Format(
SourcePausePointConstants.HotReloadCompiledLineMapLineDriftWarningFormat,
ForwardSlashFile,
17,
"return 1;",
"return 2;");
string[] compiledLines =
{
"class Sample",
" return 2;",
" return 1;",
" return 2;",
" return 2;"
};

string warning = PausePointEnableWarnings.AppendCandidateCompiledLinesToDriftWarningOrUnchanged(
drift,
"return 2;",
compiledLines);

Assert.That(
warning,
Is.EqualTo(
drift + " Candidate: the edited line's text appears at lines 2, 4, 5 in the last compiled source."));
}

/// <summary>
/// What: more than three trimmed compiled-source matches append the first three and a truncation note.
/// </summary>
[Test]
public void AppendCandidateCompiledLinesToDriftWarningOrUnchanged_WhenMoreThanThreeLinesMatch_CapsAtFirstThree()
{
string drift = string.Format(
SourcePausePointConstants.HotReloadCompiledLineMapLineDriftWarningFormat,
ForwardSlashFile,
17,
"return 1;",
"return 2;");
string[] compiledLines =
{
" return 2;",
" return 1;",
" return 2;",
" return 2;",
" return 2;"
};

string warning = PausePointEnableWarnings.AppendCandidateCompiledLinesToDriftWarningOrUnchanged(
drift,
"return 2;",
compiledLines);

Assert.That(
warning,
Is.EqualTo(
drift + " Candidate: the edited line's text appears at lines 1, 3, 4 (first 3 matches) in the last compiled source."));
}

/// <summary>
/// What: no compiled-source match leaves the drift warning unchanged.
/// </summary>
[Test]
public void AppendCandidateCompiledLinesToDriftWarningOrUnchanged_WhenNoLineMatches_LeavesWarningUnchanged()
{
string drift = string.Format(
SourcePausePointConstants.HotReloadCompiledLineMapLineDriftWarningFormat,
ForwardSlashFile,
17,
"return 1;",
"return 2;");
string[] compiledLines =
{
"class Sample",
" return 1;"
};

string warning = PausePointEnableWarnings.AppendCandidateCompiledLinesToDriftWarningOrUnchanged(
drift,
"return 2;",
compiledLines);

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

/// <summary>
/// What: an empty drift warning stays empty even when compiled source contains the edited text.
/// </summary>
[Test]
public void AppendCandidateCompiledLinesToDriftWarningOrUnchanged_WhenDriftIsEmpty_ReturnsEmpty()
{
string[] compiledLines =
{
" return 2;"
};

string warning = PausePointEnableWarnings.AppendCandidateCompiledLinesToDriftWarningOrUnchanged(
string.Empty,
"return 2;",
compiledLines);

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

/// <summary>
/// What: retarget warning interpolates resolved method, requested line, and edited span.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,94 @@ internal static string AppendCompiledMethodSpanToDriftWarningOrUnchanged(
compiledMethodEndLine);
}

// Why only after a non-empty drift warning: a candidate list without drift would look
// like a second resolution, and empty edited text never produces drift in the first place.
internal static string AppendCandidateCompiledLinesToDriftWarningOrUnchanged(
string driftWarning,
string editedLineText,
IReadOnlyList<string> compiledSourceLines)
{
if (string.IsNullOrEmpty(driftWarning))
{
return driftWarning ?? string.Empty;
}

if (string.IsNullOrEmpty(editedLineText) || compiledSourceLines == null)
{
return driftWarning;
}

string editedTrimmed = editedLineText.Trim();
if (editedTrimmed.Length == 0)
{
return driftWarning;
}

(List<int> matches, bool truncated) = CollectCandidateCompiledLineNumbers(
editedTrimmed,
compiledSourceLines);
if (matches.Count == 0)
{
return driftWarning;
}

return driftWarning + FormatCandidateCompiledLinesSuffix(matches, truncated);
}

private static (List<int> matches, bool truncated) CollectCandidateCompiledLineNumbers(
string editedTrimmed,
IReadOnlyList<string> compiledSourceLines)
{
int matchLimit = SourcePausePointConstants.CompiledLineDriftCandidateMatchLimit;
List<int> matches = new List<int>();
bool truncated = false;
for (int index = 0; index < compiledSourceLines.Count; index++)
{
string compiledLine = compiledSourceLines[index];
if (compiledLine == null)
{
continue;
}

if (!string.Equals(compiledLine.Trim(), editedTrimmed, StringComparison.Ordinal))
{
continue;
}

if (matches.Count == matchLimit)
{
truncated = true;
break;
}

matches.Add(index + 1);
}

return (matches, truncated);
}

private static string FormatCandidateCompiledLinesSuffix(List<int> matches, bool truncated)
{
if (matches.Count == 1 && !truncated)
{
return string.Format(
SourcePausePointConstants.HotReloadCompiledLineDriftCandidateSingleFormat,
matches[0]);
}

string listed = string.Join(", ", matches);
if (truncated)
{
listed += string.Format(
SourcePausePointConstants.HotReloadCompiledLineDriftCandidateTruncatedMatchesSuffixFormat,
SourcePausePointConstants.CompiledLineDriftCandidateMatchLimit);
}

return string.Format(
SourcePausePointConstants.HotReloadCompiledLineDriftCandidateMultipleFormat,
listed);
}

internal static string BuildRetargetedToHotReloadPatchWarningOrEmpty(
bool retargetedToHotReloadPatch,
string resolvedMethod,
Expand Down
49 changes: 22 additions & 27 deletions Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -350,12 +350,18 @@ private static PausePointResponse FinishEnableBySourceLocation(
snapshot = UloopPausePointRegistry.GetStatus(id);
}

string resolvedLineText = ResolveEnableLineText(
parameters.File,
resolvedLine,
resolvedEndLine,
retargetedToHotReloadPatch,
hasActiveHotReloadPatches);
bool compareCompiledLineDrift = hasActiveHotReloadPatches && !retargetedToHotReloadPatch;
string compiledSnapshotSource = compareCompiledLineDrift
? LoadCompiledSnapshotSourceOrEmpty(parameters.File)
: string.Empty;
// Why snapshot over disk: the editor file may already include unpatched-line drift, so
// reading disk at the compiled ResolvedLine shows the wrong statement (FB9 empty/mismatch).
// The snapshot read stays single-line because the verified snapshot has no end-line data;
// the disk read spans resolvedLine..resolvedEndLine so a rounded-forward multi-line
// statement returns its full text.
string resolvedLineText = compareCompiledLineDrift
? SourcePausePointSourceLineReader.ReadLineTextFromSource(compiledSnapshotSource, resolvedLine)
: PausePointLineTextReader.ReadResolvedLineText(parameters.File, resolvedLine, resolvedEndLine);
UloopPausePointRegistry.SetResolvedLine(id, resolvedLine, resolvedLineText);

PausePointResponse response = PausePointResponse.FromSnapshot(snapshot);
Expand All @@ -373,7 +379,6 @@ private static PausePointResponse FinishEnableBySourceLocation(
parameters.Line,
editedMethodStartLine,
editedMethodEndLine));
bool compareCompiledLineDrift = hasActiveHotReloadPatches && !retargetedToHotReloadPatch;
string compiledLineMapWarning = PausePointEnableWarnings.ChooseCompiledLineMapWarning(
patchedMethodPdbUnavailableWarning,
PausePointEnableWarnings.BuildCompiledLineMapWarningOrEmpty(
Expand All @@ -393,6 +398,11 @@ private static PausePointResponse FinishEnableBySourceLocation(
resolvedMethod,
compiledMethodStartLine,
compiledMethodEndLine);
string[] compiledSourceLines = SourcePausePointSourceLineReader.SplitSourceLines(compiledSnapshotSource);
driftWarning = PausePointEnableWarnings.AppendCandidateCompiledLinesToDriftWarningOrUnchanged(
driftWarning,
editedLineText,
compiledSourceLines);
enableWarning = PausePointEnableWarnings.MergeWarnings(enableWarning, driftWarning);
if (driftWarning.Length > 0)
{
Expand Down Expand Up @@ -466,27 +476,12 @@ private static void LogPhysicsDispatchDiagnostics(string operation, string id, T
});
}

// Why snapshot over disk: the editor file may already include unpatched-line drift, so
// reading disk at the compiled ResolvedLine shows the wrong statement (FB9 empty/mismatch).
// The snapshot read stays single-line because the verified snapshot has no end-line data;
// the disk read spans resolvedLine..resolvedEndLine so a rounded-forward multi-line
// statement returns its full text.
private static string ResolveEnableLineText(
string requestedFile,
int resolvedLine,
int resolvedEndLine,
bool retargetedToHotReloadPatch,
bool hasActiveHotReloadPatches)
private static string LoadCompiledSnapshotSourceOrEmpty(string requestedFile)
{
if (hasActiveHotReloadPatches && !retargetedToHotReloadPatch)
{
string normalizedFile = SourcePausePointPathNormalizer.ToForwardSlashes(requestedFile);
string snapshotSource =
HotReloadPausePointCoordination.GetVerifiedSnapshotSourceForFile?.Invoke(normalizedFile);
return SourcePausePointSourceLineReader.ReadLineTextFromSource(snapshotSource, resolvedLine);
}

return PausePointLineTextReader.ReadResolvedLineText(requestedFile, resolvedLine, resolvedEndLine);
string normalizedFile = SourcePausePointPathNormalizer.ToForwardSlashes(requestedFile);
string snapshotSource =
HotReloadPausePointCoordination.GetVerifiedSnapshotSourceForFile?.Invoke(normalizedFile);
return snapshotSource ?? string.Empty;
}

// The derived id must use the originally requested file/line (not the resolved/rounded
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,23 @@ internal static class SourcePausePointConstants
public const string HotReloadCompiledMethodSpanInLastCompiledSourceFormat =
" In the last compiled source, '{0}' spans lines {1}-{2}.";

// Why cap 3: a longer match list turns the enable warning into another line-number puzzle.
public const int CompiledLineDriftCandidateMatchLimit = 3;

// Format: 1-based compiled line number. Why "Candidate": this is a search hit, not a
// guarantee that re-enabling there is the intended statement.
public const string HotReloadCompiledLineDriftCandidateSingleFormat =
" Candidate: the edited line's text appears at line {0} in the last compiled source.";

// Format: comma-separated 1-based compiled line numbers, with an optional truncation note.
public const string HotReloadCompiledLineDriftCandidateMultipleFormat =
" Candidate: the edited line's text appears at lines {0} in the last compiled source.";

// Why format from CompiledLineDriftCandidateMatchLimit: a hard-coded "3" would lie
// if the cap changed.
public const string HotReloadCompiledLineDriftCandidateTruncatedMatchesSuffixFormat =
" (first {0} matches)";

public const string NearbyCompiledMethodsPrefix =
" Nearby methods in the last compiled source: ";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -34,14 +35,24 @@ public static string ReadLineText(string absoluteFilePath, int startLine, int en
return string.Join(" ", trimmedNonEmptyLines);
}

public static string[] SplitSourceLines(string sourceText)
{
if (string.IsNullOrEmpty(sourceText))
{
return Array.Empty<string>();
}

return sourceText.Replace("\r\n", "\n").Split('\n');
}

public static string ReadLineTextFromSource(string sourceText, int lineNumber)
{
if (string.IsNullOrEmpty(sourceText) || lineNumber <= 0)
{
return string.Empty;
}

string[] lines = sourceText.Replace("\r\n", "\n").Split('\n');
string[] lines = SplitSourceLines(sourceText);
if (lineNumber > lines.Length)
{
return string.Empty;
Expand Down
Loading