Fix flaky ModifiedPathsTests by replaying the on-disk log#2017
Merged
tyrielv merged 2 commits intoJun 11, 2026
Conversation
The modified-paths database file is an append-only log of "A path" /
"D path" entries that ModifiedPathsDatabase compacts at the end of each
background-op batch via WriteAllEntriesAndFlush in PostBackgroundOperation.
BackgroundOperationCount, however, drops to 0 inside DequeueAndFlush for
the last task -- before the post-callback runs. WaitForBackgroundOperations
polls until count == 0 and can therefore return between dequeue and
compaction, leaving the file in a state like:
A temp.txt
D temp.txt
ModifiedPathsShouldNotContain matched both lines, fed two results into
EnumerableShouldExtensions.ShouldNotContain, and the SingleOrDefault call
threw "Sequence contains more than one matching element" -- masking the
underlying race with a confusing exception.
The product code is correct: FileBasedCollection.TryLoadFromDisk replays
the A/D log on mount, so the on-disk state is always recoverable. This
change fixes the tests:
* GVFSHelpers.ModifiedPathsShouldContain / ModifiedPathsShouldNotContain
now build the current set by replaying the A/D log (same algorithm as
TryLoadFromDisk) and check membership in that set. This matches the
semantic intent of every existing caller and is robust to the
flush-race window.
* ModifiedPathsContentsShouldEqual is renamed to
ModifiedPathsRawFileContentsShouldEqual and gains an XML doc comment
pointing callers at the semantic helpers unless they specifically need
to validate the compacted file layout. The two existing callers in
CheckoutTests are updated.
* EnumerableShouldExtensions.ShouldNotContain uses Where(predicate)
instead of SingleOrDefault(predicate) so multi-match cases produce a
useful Assert.Fail message instead of InvalidOperationException.
* A new ModifiedPathsDatabaseTests.AddFollowedByDeleteIsRecoveredOnLoad
unit test pins down the recovery contract -- loading
"A temp.txt\r\nD temp.txt\r\n" produces an empty set
(only the auto-added .gitattributes entry).
Assisted-by: Claude Opus 4.7
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Per Keith's review, the two CheckoutTests callsites that used ModifiedPathsRawFileContentsShouldEqual were exposed to the same compaction race the rest of this PR fixes -- WaitForBackgroundOperations can return before PostBackgroundOperation rewrites the file, so any transient "A path / D path" cycle (today: none; future: easy to introduce) would break the exact-equals raw-bytes assertion. Those callers' real intent is semantic: "after this checkout sequence, the only modified path is .gitattributes". Replace the raw helper with a new ModifiedPathsShouldOnlyContain that compares against the replayed A/D log as a set. The raw helper now has zero callers and is removed. Assisted-by: Claude Opus 4.7 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
KeithIsSleeping
approved these changes
Jun 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the flaky
ModifiedPathsTests.DeletedTempFileIsRemovedFromModifiedFilestest (and any other modified-paths assertion that hits the same race).Root cause
ModifiedPathsDatabaseis an append-only A/D log that gets compacted at the end of each background-op batch viaWriteAllEntriesAndFlushinPostBackgroundOperation.BackgroundOperationCountdrops to 0 insideDequeueAndFlushfor the last task — before the post-callback (and therefore the compaction) runs.WaitForBackgroundOperationspolls until count==0 and can return between those two steps, observing a transient state like:The old
ModifiedPathsShouldNotContainmatched both lines, fed two results intoEnumerableShouldExtensions.ShouldNotContain, andSingleOrDefaultthrewInvalidOperationException— masking the real race with a confusing exception.Product code is correct
FileBasedCollection.TryLoadFromDiskreplays the A/D log on mount and only the final state reaches the in-memory set, so the on-disk file is always recoverable. The append-then-amortized-rewrite pattern is intentional (one O(1) write per FS event vs. O(N) on every op). No product change needed. The newAddFollowedByDeleteIsRecoveredOnLoadunit test pins down this recovery contract explicitly.Changes (all test-only)
GVFSHelpers.ModifiedPathsShouldContain/ModifiedPathsShouldNotContain— Replay the A/D log into aHashSet<string>(same algorithm asTryLoadFromDisk) and check semantic membership. Matches the intent of all ~110 existing callers and is robust to the flush-race window.GVFSHelpers.ModifiedPathsContentsShouldEqual→ModifiedPathsRawFileContentsShouldEqual— Renamed and documented to make it obvious this asserts on the raw compacted file layout, not the semantic set. Two callers inCheckoutTests.csupdated.EnumerableShouldExtensions.ShouldNotContain<T>(predicate)— UsesWhere(predicate)instead ofSingleOrDefault(predicate)so multi-match cases yield a usefulAssert.Failmessage instead ofInvalidOperationException.ModifiedPathsDatabaseTests.AddFollowedByDeleteIsRecoveredOnLoad— New unit test asserting that loading"A temp.txt\r\nD temp.txt\r\n"produces an empty set (only the default.gitattributes).Verification
GVFS.UnitTestssuite passes (819/819) on Windows.AddFollowedByDeleteIsRecoveredOnLoadtest passes.GVFS.FunctionalTests.csprojbuilds clean.