diff --git a/src/DiffEngineViewer.Tests/HeldDeleteTests.cs b/src/DiffEngineViewer.Tests/HeldDeleteTests.cs new file mode 100644 index 00000000..01e467e6 --- /dev/null +++ b/src/DiffEngineViewer.Tests/HeldDeleteTests.cs @@ -0,0 +1,69 @@ +/// +/// When a sweep holds its deletes. A snapshot moving inline arrives as two unrelated entries - the +/// patch and a delete of the verified file it replaces - so a patch the applier will not take must +/// stop the delete, or the snapshot loses both copies at once. +/// +public class HeldDeleteTests +{ + /// + /// A refusal recorded on a conflicted entry belongs to the targeted accept that made it, not + /// to the batch reading it. A bulk accept hands conflicted entries back untouched, so that + /// status sat there and held every pending delete on every later accept-all, citing a refusal + /// that had not happened. + /// + [Test] + public async Task A_refusal_left_on_a_conflicted_entry_does_not_hold_a_later_sweep() + { + var state = Conflicted(); + + // One variant accepted on its own, and refused - the file was locked, say. The entry + // stays, conflicted still, and carries what the applier said + var refused = ViewerSession.Apply( + state, + CommandKind.Accept, + Sweeping(InlineApplyResult.Failed("Failed to write: BTests.cs"))); + await Assert.That(refused.Queue.Single(_ => _.Kind == QueueEntryKind.Inline).Status).IsNotNull(); + + var swept = ViewerSession.Apply(refused, CommandKind.AcceptAll, Sweeping(InlineApplyResult.Applied)); + await Assert.That(swept.Queue.Where(_ => _.Kind == QueueEntryKind.Delete)).IsEmpty(); + } + + /// + /// The reason the read exists at all: a patch this batch could not write still holds the + /// deletes. + /// + [Test] + public async Task A_refusal_in_this_sweep_holds_the_deletes() + { + var state = ViewerSession.EnqueueTracked( + Fixtures.Inline(Fixtures.Patch("A.cs", 1)), + Fixtures.Delete()); + + var swept = ViewerSession.Apply( + state, + CommandKind.AcceptAll, + Sweeping(InlineApplyResult.NotFound("Could not locate the VerifyInline call"))); + + var delete = swept.Queue.Single(_ => _.Kind == QueueEntryKind.Delete); + await Assert.That(delete.Status).StartsWith("Held:"); + } + + /// + /// A delete that is actually deletable, so that a held one and a failed one cannot be confused + /// for each other: the fixture's default actions throw for anything touching a file. + /// + static ViewerActions Sweeping(InlineApplyResult result) => + Fixtures.Applying(result) with + { + DeleteFile = static _ => + { + } + }; + + static SessionState Conflicted() => + ViewerSession.EnqueueTracked( + Fixtures.Inline( + Fixtures.Patch(content: "eight", framework: "net8.0"), + Fixtures.Patch(content: "nine", framework: "net9.0")), + Fixtures.Delete()); +} diff --git a/src/DiffEngineViewer/ViewerSession.cs b/src/DiffEngineViewer/ViewerSession.cs index 245a246d..f9f7abac 100644 --- a/src/DiffEngineViewer/ViewerSession.cs +++ b/src/DiffEngineViewer/ViewerSession.cs @@ -630,8 +630,12 @@ static SessionState SweepTracked( /// not the last copy of it leaving. /// /// - /// A status on an inline entry is the outcome of an attempt, so this reads failures only: - /// conflicted entries that a bulk accept skips are handed back untouched and carry none. + /// A status on an inline entry is the outcome of an attempt, and this has to read only the + /// attempts the sweep it is answering for made. A bulk accept skips conflicted entries and + /// hands them back exactly as they were, so a status on one of those was left by a targeted + /// accept of a single variant, at some earlier point - and reading it held every pending + /// delete on every accept-all after it, citing a refusal in a batch that never touched the + /// entry. Everything else left in the queue with a status was tried and refused just now. /// Discarding asks nothing of the patches, so it sweeps as it always did. /// /// @@ -644,7 +648,7 @@ static bool InlineRefused(IReadOnlyList queue, bool discarding) foreach (var entry in queue) { - if (entry is {Kind: QueueEntryKind.Inline, Status: not null}) + if (entry is {Kind: QueueEntryKind.Inline, Conflicted: false, Status: not null}) { return true; }