diff --git a/Assets/Tests/Editor/PausePointCompiledLineMapWarningTests.cs b/Assets/Tests/Editor/PausePointCompiledLineMapWarningTests.cs index 668da781cd..d253c97be3 100644 --- a/Assets/Tests/Editor/PausePointCompiledLineMapWarningTests.cs +++ b/Assets/Tests/Editor/PausePointCompiledLineMapWarningTests.cs @@ -932,6 +932,291 @@ public void AppendRequestedLineCandidateCompiledLinesToDriftWarningOrUnchanged_W + " Candidate: the text at --line 107 in the edited file appears at lines 1, 3, 4 (first 3 matches) in the last compiled source.")); } + /// + /// What: one compiled-source match for the requested --line text is appended to a + /// resolve-failure Message. + /// + [Test] + public void AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged_WhenOneLineMatches_AppendsCandidate() + { + const string message = + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'."; + string[] compiledLines = new string[110]; + for (int index = 0; index < compiledLines.Length; index++) + { + compiledLines[index] = " return 0;"; + } + + compiledLines[109] = " return 2;"; + + string result = PausePointEnableWarnings.AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged( + message, + 116, + " return 2; ", + compiledLines); + + Assert.That( + result, + Is.EqualTo( + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'." + + " Candidate: the text at --line 116 in the edited file appears at line 110 in the last compiled source.")); + } + + /// + /// What: two compiled-source matches for the requested --line text append both line numbers. + /// + [Test] + public void AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged_WhenTwoLinesMatch_AppendsBothCandidates() + { + const string message = + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'."; + string[] compiledLines = + { + "class Sample", + " return 2;", + " return 1;", + " return 2;" + }; + + string result = PausePointEnableWarnings.AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged( + message, + 116, + "return 2;", + compiledLines); + + Assert.That( + result, + Is.EqualTo( + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'." + + " Candidate: the text at --line 116 in the edited file appears at lines 2, 4 in the last compiled source.")); + } + + /// + /// What: more than three compiled-source matches for a resolve-failure Message cap at the + /// first three. + /// + [Test] + public void AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged_WhenMoreThanThreeLinesMatch_CapsAtFirstThree() + { + const string message = + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'."; + string[] compiledLines = + { + " return 2;", + " return 1;", + " return 2;", + " return 2;", + " return 2;" + }; + + string result = PausePointEnableWarnings.AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged( + message, + 116, + "return 2;", + compiledLines); + + Assert.That( + result, + Is.EqualTo( + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'." + + " Candidate: the text at --line 116 in the edited file appears at lines 1, 3, 4 (first 3 matches) in the last compiled source.")); + } + + /// + /// What: a resolve-failure Message stays unchanged when compiled source has no matching line. + /// + [Test] + public void AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged_WhenNoLineMatches_ReturnsUnchanged() + { + const string message = + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'."; + string[] compiledLines = + { + " return 0;" + }; + + string result = PausePointEnableWarnings.AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged( + message, + 116, + "return 2;", + compiledLines); + + Assert.That(result, Is.EqualTo(message)); + } + + /// + /// What: a resolve-failure Message stays unchanged when the edited line text is blank. + /// + [Test] + public void AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged_WhenEditedTextEmpty_ReturnsUnchanged() + { + const string message = + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'."; + string[] compiledLines = + { + " return 2;" + }; + + string result = PausePointEnableWarnings.AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged( + message, + 116, + " ", + compiledLines); + + Assert.That(result, Is.EqualTo(message)); + } + + /// + /// What: a resolve-failure Message stays unchanged when compiled source lines are null. + /// + [Test] + public void AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged_WhenCompiledSourceLinesNull_ReturnsUnchanged() + { + const string message = + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'."; + + string result = PausePointEnableWarnings.AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged( + message, + 116, + "return 2;", + null); + + Assert.That(result, Is.EqualTo(message)); + } + + /// + /// What: an active hot-reload gate plus a compiled-source match appends Candidate after + /// Nearby methods on a resolve-failure Message. + /// + [Test] + public void BuildResolveFailureMessage_WhenHotReloadGateTrueAndLineMatches_AppendsNearbyThenCandidate() + { + const string errorMessage = + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'."; + SourcePausePointNearbyCompiledMethod[] nearby = + { + new SourcePausePointNearbyCompiledMethod("Enemy.Update", 100, 120) + }; + string[] compiledLines = + { + " return 2;" + }; + + string result = PausePointEnableWarnings.BuildResolveFailureMessage( + errorMessage, + nearby, + true, + 116, + true, + "return 2;", + compiledLines); + + Assert.That( + result, + Is.EqualTo( + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'." + + " Nearby methods in the last compiled source: 'Enemy.Update' spans lines 100-120." + + " Candidate: the text at --line 116 in the edited file appears at line 1 in the last compiled source.")); + } + + /// + /// What: the same matching inputs without the hot-reload gate keep Nearby methods and omit + /// Candidate. + /// + [Test] + public void BuildResolveFailureMessage_WhenHotReloadGateFalseAndLineMatches_AppendsNearbyOnly() + { + const string errorMessage = + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'."; + SourcePausePointNearbyCompiledMethod[] nearby = + { + new SourcePausePointNearbyCompiledMethod("Enemy.Update", 100, 120) + }; + string[] compiledLines = + { + " return 2;" + }; + + string result = PausePointEnableWarnings.BuildResolveFailureMessage( + errorMessage, + nearby, + false, + 116, + true, + "return 2;", + compiledLines); + + Assert.That( + result, + Is.EqualTo( + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'." + + " Nearby methods in the last compiled source: 'Enemy.Update' spans lines 100-120.")); + } + + /// + /// What: a null compiled-source list under an active hot-reload gate keeps Nearby methods + /// and omits Candidate. + /// + [Test] + public void BuildResolveFailureMessage_WhenCompiledSourceLinesNull_AppendsNearbyOnly() + { + const string errorMessage = + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'."; + SourcePausePointNearbyCompiledMethod[] nearby = + { + new SourcePausePointNearbyCompiledMethod("Enemy.Update", 100, 120) + }; + + string result = PausePointEnableWarnings.BuildResolveFailureMessage( + errorMessage, + nearby, + true, + 116, + true, + "return 2;", + null); + + Assert.That( + result, + Is.EqualTo( + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'." + + " Nearby methods in the last compiled source: 'Enemy.Update' spans lines 100-120.")); + } + + /// + /// What: a failed edited-line read under an active hot-reload gate keeps Nearby methods and + /// omits Candidate even when compiled source would match. + /// + [Test] + public void BuildResolveFailureMessage_WhenRequestedLineReadFails_AppendsNearbyOnly() + { + const string errorMessage = + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'."; + SourcePausePointNearbyCompiledMethod[] nearby = + { + new SourcePausePointNearbyCompiledMethod("Enemy.Update", 100, 120) + }; + string[] compiledLines = + { + " return 2;" + }; + + string result = PausePointEnableWarnings.BuildResolveFailureMessage( + errorMessage, + nearby, + true, + 116, + false, + "return 2;", + compiledLines); + + Assert.That( + result, + Is.EqualTo( + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'." + + " Nearby methods in the last compiled source: 'Enemy.Update' spans lines 100-120.")); + } + /// /// What: retarget warning interpolates resolved method, requested line, and edited span. /// diff --git a/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointEnableWarnings.cs b/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointEnableWarnings.cs index d93bcd2e62..df3f3fd4c1 100644 --- a/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointEnableWarnings.cs +++ b/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointEnableWarnings.cs @@ -231,6 +231,76 @@ internal static string AppendRequestedLineCandidateCompiledLinesToDriftWarningOr truncated); } + /// + /// Appends compiled-line Candidate text to a resolve-failure Message when the edited + /// --line text appears in the last compiled source. + /// + internal static string AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged( + string message, + int requestedLine, + string requestedLineEditedText, + IReadOnlyList compiledSourceLines) + { + if (string.IsNullOrEmpty(message) || requestedLine <= 0) + { + return message ?? string.Empty; + } + + if (string.IsNullOrEmpty(requestedLineEditedText) || compiledSourceLines == null) + { + return message; + } + + string editedTrimmed = requestedLineEditedText.Trim(); + if (editedTrimmed.Length == 0) + { + return message; + } + + (List matches, bool truncated) = CollectCandidateCompiledLineNumbers( + editedTrimmed, + compiledSourceLines); + if (matches.Count == 0) + { + return message; + } + + // Why no drift-warning gate: resolve-failure Messages have no drift warning, and + // Candidate is the only compiled-line number the caller can retry with. + return message + FormatRequestedLineCandidateCompiledLinesSuffix( + requestedLine, + matches, + truncated); + } + + /// + /// Builds a resolve-failure Message: Nearby methods, then Candidate when hot-reload + /// patches are active and the edited --line text was read. + /// + internal static string BuildResolveFailureMessage( + string errorMessage, + IReadOnlyList nearbyCompiledMethods, + bool hasActiveHotReloadPatches, + int requestedLine, + bool requestedLineReadOk, + string requestedLineEditedText, + IReadOnlyList compiledSourceLinesOrNull) + { + string message = AppendNearbyCompiledMethodsSuffix(errorMessage, nearbyCompiledMethods); + // Why skip Candidate when the edited line was not read: the Candidate sentence names + // the text at --line N in the edited file, which is false if that read failed. + if (!hasActiveHotReloadPatches || !requestedLineReadOk) + { + return message; + } + + return AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged( + message, + requestedLine, + requestedLineEditedText, + compiledSourceLinesOrNull); + } + private static (List matches, bool truncated) CollectCandidateCompiledLineNumbers( string editedTrimmed, IReadOnlyList compiledSourceLines) diff --git a/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs b/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs index 92d52535b1..f582dabf8c 100644 --- a/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs +++ b/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs @@ -273,10 +273,34 @@ private static PausePointResponse EnableBySourceLocation(EnablePausePointSchema string recommendedNextAction = hasActiveHotReloadPatches ? SourcePausePointConstants.HotReloadCompiledLineMapResolveFailureNextAction : SourcePausePointConstants.ResolveFailedRecommendedNextAction; + IReadOnlyList compiledSourceLinesOrNull = null; + bool requestedLineReadOk = false; + string requestedLineEditedText = string.Empty; + // Why skip snapshot/edited-line IO without patches: Candidate is omitted on that + // path, so those reads would change the historical no-patch failure for no gain. + if (hasActiveHotReloadPatches) + { + string compiledSnapshotSource = LoadCompiledSnapshotSourceOrEmpty(parameters.File); + compiledSourceLinesOrNull = string.IsNullOrEmpty(compiledSnapshotSource) + ? null + : SourcePausePointSourceLineReader.SplitSourceLines(compiledSnapshotSource); + (requestedLineReadOk, requestedLineEditedText) = + PausePointCompiledLineComparisonWarnings.ReadEditedLineText( + parameters.File, + parameters.Line); + } + + string message = PausePointEnableWarnings.BuildResolveFailureMessage( + resolveResult.ErrorMessage, + resolveResult.NearbyCompiledMethods, + hasActiveHotReloadPatches, + parameters.Line, + requestedLineReadOk, + requestedLineEditedText, + compiledSourceLinesOrNull); + PausePointResponse response = CreateValidationFailure( - PausePointEnableWarnings.AppendNearbyCompiledMethodsSuffix( - resolveResult.ErrorMessage, - resolveResult.NearbyCompiledMethods), + message, SourcePausePointConstants.ErrorCodeResolveFailed, recommendedNextAction); response.Warning = PausePointEnableWarnings.ChooseCompiledLineMapWarning(