Browse Source

Await debug-step replay decompiles to fix flaky UI tests

The Debug Steps "show state before/after" replay re-decompiles the active
tab; the headless UI tests inferred completion by polling IsDecompiling +
Text, which can return on stale state or hit the 60s wait deadline when
that shared signal races under CI load (the observed intermittent CI
timeout). RestartDecompileWithStepLimit now returns the decompile Task so
the replay completion can be awaited deterministically; the tests await it
instead of polling. The production IsDecompiling reset is unchanged -- the
last decompile's finally always resets it; the race was only in the test's
completion inference.

Assisted-by: Claude:claude-opus-4-8:Claude Code
pull/3847/head
Siegfried Pammer 1 week ago committed by Siegfried Pammer
parent
commit
bf2f57106a
  1. 12
      ILSpy.Tests/Views/DebugStepsTests.cs
  2. 20
      ILSpy/TextView/DecompilerTabPageModel.cs

12
ILSpy.Tests/Views/DebugStepsTests.cs

@ -172,15 +172,13 @@ public class DebugStepsTests @@ -172,15 +172,13 @@ public class DebugStepsTests
var replayStep = transformGroupWithChanges.Children.First();
var tab = vm.DockWorkspace.ActiveDecompilerTab!;
tab.RestartDecompileWithStepLimit(replayStep.BeginStep, isDebug: false, replayStep.BeginStep);
tab = await vm.DockWorkspace.WaitForDecompiledTextAsync();
await tab.RestartDecompileWithStepLimit(replayStep.BeginStep, isDebug: false, replayStep.BeginStep);
tab.Text.Should().NotBeNullOrWhiteSpace("C# replay before a selected AST mutation step must still emit code");
tab.DebugStepHighlight.Should().NotBeNull("C# replay before a selected AST mutation step must locate the changed node");
debugStepsVm.Steps.Should().BeSameAs(collectedSteps,
"a step-limited C# replay must not replace the full step tree shown by the pane");
tab.RestartDecompileWithStepLimit(replayStep.EndStep, isDebug: false, replayStep.BeginStep);
tab = await vm.DockWorkspace.WaitForDecompiledTextAsync();
await tab.RestartDecompileWithStepLimit(replayStep.EndStep, isDebug: false, replayStep.BeginStep);
tab.Text.Should().NotBeNullOrWhiteSpace("C# replay after a selected AST mutation step must still emit code");
tab.DebugStepHighlight.Should().NotBeNull("C# replay after a selected AST mutation step must locate the changed node");
debugStepsVm.Steps.Should().BeSameAs(collectedSteps,
@ -226,15 +224,13 @@ public class DebugStepsTests @@ -226,15 +224,13 @@ public class DebugStepsTests
var collectedSteps = debugStepsVm.Steps;
var tab = vm.DockWorkspace.ActiveDecompilerTab!;
tab.RestartDecompileWithStepLimit(replayStep!.BeginStep, isDebug: false, replayStep.BeginStep);
tab = await vm.DockWorkspace.WaitForDecompiledTextAsync();
await tab.RestartDecompileWithStepLimit(replayStep!.BeginStep, isDebug: false, replayStep.BeginStep);
tab.Text.Should().NotBeNullOrWhiteSpace("ILAst replay before a selected step must still emit IL");
tab.DebugStepHighlight.Should().NotBeNull("ILAst replay before a selected step must locate the changed instruction");
debugStepsVm.Steps.Should().BeSameAs(collectedSteps,
"a step-limited ILAst replay must not replace the full step tree shown by the pane");
tab.RestartDecompileWithStepLimit(replayStep.EndStep, isDebug: false, replayStep.BeginStep);
tab = await vm.DockWorkspace.WaitForDecompiledTextAsync();
await tab.RestartDecompileWithStepLimit(replayStep.EndStep, isDebug: false, replayStep.BeginStep);
tab.Text.Should().NotBeNullOrWhiteSpace("ILAst replay after a selected step must still emit IL");
tab.DebugStepHighlight.Should().NotBeNull("ILAst replay after a selected step must locate the changed instruction");

20
ILSpy/TextView/DecompilerTabPageModel.cs

@ -401,12 +401,12 @@ namespace ICSharpCode.ILSpy.TextView @@ -401,12 +401,12 @@ namespace ICSharpCode.ILSpy.TextView
/// is <see cref="int.MaxValue"/>). <paramref name="isDebug"/> toggles the transforms'
/// verbose-debug emission. No-op when there's nothing currently being decompiled.
/// </summary>
public void RestartDecompileWithStepLimit(int stepLimit, bool isDebug, int? highlightStep = null)
public Task RestartDecompileWithStepLimit(int stepLimit, bool isDebug, int? highlightStep = null)
{
pendingStepLimit = stepLimit;
pendingHighlightStep = highlightStep;
pendingIsDebug = isDebug;
StartDecompile();
return StartDecompile();
}
/// <summary>Re-runs the current decompile with a larger output-length limit (the "Display code
@ -463,16 +463,20 @@ namespace ICSharpCode.ILSpy.TextView @@ -463,16 +463,20 @@ namespace ICSharpCode.ILSpy.TextView
ICSharpCode.ILSpy.Commands.SaveCodeHelper.SaveNodeAsync(node, languageService, dockWorkspace).HandleExceptions();
}
// Fire-and-forget wrapper around DecompileAsync that observes the resulting Task.
// Without this, exceptions raised by the dispatched property setters (e.g. the
// PropertyChanged subscribers in DecompilerTextView) become UnobservedTaskException
// faults that the finalizer thread rethrows much later, hiding the originating bug.
void StartDecompile()
// Starts a decompile and returns the Task that completes once the output has been applied,
// so callers (e.g. the Debug Steps replay, and tests) can await completion deterministically
// instead of polling IsDecompiling/Text. A fault-observing continuation is attached so that
// exceptions raised by the dispatched property setters (e.g. the PropertyChanged subscribers
// in DecompilerTextView) don't become UnobservedTaskException faults the finalizer thread
// rethrows much later, hiding the originating bug.
Task StartDecompile()
{
pendingDecompile = DecompileAsync().ContinueWith(t => {
var decompile = DecompileAsync();
pendingDecompile = decompile.ContinueWith(t => {
if (t.Exception is { } ex)
System.Diagnostics.Debug.WriteLine($"DecompileAsync faulted: {ex.Flatten()}");
}, TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously);
return decompile;
}
/// <summary>

Loading…
Cancel
Save