diff --git a/src/DiffEngine.Tests/TrackedKeyCaseTests.cs b/src/DiffEngine.Tests/TrackedKeyCaseTests.cs new file mode 100644 index 00000000..ab42e2cf --- /dev/null +++ b/src/DiffEngine.Tests/TrackedKeyCaseTests.cs @@ -0,0 +1,37 @@ +/// +/// Tracked keys fold case exactly where inline keys do. +/// +/// TrackedKeys lower-cased unconditionally while InlineKey folds only on Windows and macOS, and +/// InlineKey says why: on Linux two paths differing only in case are two files, and giving them +/// one key means the second takes over the first's entry. For a tracked move that shows up as +/// ViewerSession.EnqueueTracked dropping one of them - a parameterised test with value=a and +/// value=A silently loses a pending file. +/// +/// +/// Asserted as agreement between the two rather than as a platform's answer, so it holds wherever +/// it runs. +/// +/// +public class TrackedKeyCaseTests +{ + const string upper = "/repo/tests/Value.txt"; + const string lower = "/repo/tests/value.txt"; + + [Test] + public async Task MoveKeysFoldWithInlineKeys() + { + var inlineFolds = InlineKey.For(upper, 1) == InlineKey.For(lower, 1); + + await Assert.That(TrackedKeys.ForMove(upper) == TrackedKeys.ForMove(lower)) + .IsEqualTo(inlineFolds); + } + + [Test] + public async Task DeleteKeysFoldWithInlineKeys() + { + var inlineFolds = InlineKey.For(upper, 1) == InlineKey.For(lower, 1); + + await Assert.That(TrackedKeys.ForDelete(upper) == TrackedKeys.ForDelete(lower)) + .IsEqualTo(inlineFolds); + } +} diff --git a/src/DiffEngine/Inline/InlineKey.cs b/src/DiffEngine/Inline/InlineKey.cs index 465d3e5c..28e49481 100644 --- a/src/DiffEngine/Inline/InlineKey.cs +++ b/src/DiffEngine/Inline/InlineKey.cs @@ -22,7 +22,14 @@ public static class InlineKey /// /// public static string For(string sourceFile, int line) => - $"{(caseInsensitivePaths ? sourceFile.ToLowerInvariant() : sourceFile)}|{line}"; + $"{FoldPath(sourceFile)}|{line}"; + + /// + /// A path folded the way this machine's file system folds it, so anything else keyed by path + /// agrees with about when two paths are one file. + /// + internal static string FoldPath(string path) => + caseInsensitivePaths ? path.ToLowerInvariant() : path; static readonly bool caseInsensitivePaths = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || diff --git a/src/DiffEngine/Protocol/TrackedKeys.cs b/src/DiffEngine/Protocol/TrackedKeys.cs index ce156c93..cfab20c9 100644 --- a/src/DiffEngine/Protocol/TrackedKeys.cs +++ b/src/DiffEngine/Protocol/TrackedKeys.cs @@ -2,9 +2,15 @@ namespace DiffEngine; /// /// Keys for the tray's tracked moves and deletes when they ride the inline listing. Prefixed so -/// they cannot collide with an inline key — a lower-cased path plus "|" plus a line number, where -/// a Windows path cannot put a colon at index four — and so only the tray has to know which +/// they cannot collide with an inline key — a folded path plus "|" plus a line number, where a +/// Windows path cannot put a colon at index four — and so only the tray has to know which /// collection a key belongs to. Every other process echoes keys back opaquely. +/// +/// Folded through , which folds only where the file system does. +/// Lower-casing unconditionally made two Linux files differing only in case - which is two files, +/// not one - share a key, and the viewer's enqueue drops the earlier of two entries with the same +/// key. A parameterised test producing value=a and value=A lost one of them. +/// /// static class TrackedKeys { @@ -13,10 +19,10 @@ static class TrackedKeys public const string DeletePrefix = "delete:"; public static string ForMove(string temp) => - MovePrefix + temp.ToLowerInvariant(); + MovePrefix + InlineKey.FoldPath(temp); public static string ForDelete(string file) => - DeletePrefix + file.ToLowerInvariant(); + DeletePrefix + InlineKey.FoldPath(file); public static bool IsTracked(string key) => key.StartsWith(MovePrefix, StringComparison.Ordinal) ||