From 0539f8d523523f873f25ca6cd8f70b90cc7bcfb8 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Thu, 24 Sep 2026 12:03:02 +1000 Subject: [PATCH] Fix FileSnippet test race and retry the unchanged-content read --- .../Processing/DirectoryMarkdownProcessor.cs | 24 ++++++++++++++++++- .../FileSnippetExplicitInclude/one.source.md | 12 ++++++++++ .../FileSnippetExplicitInclude/sourceFile.txt | 1 + src/Tests/DirectoryMarkdownProcessorTests.cs | 3 ++- 4 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 src/Tests/DirectoryMarkdownProcessor/FileSnippetExplicitInclude/one.source.md create mode 100644 src/Tests/DirectoryMarkdownProcessor/FileSnippetExplicitInclude/sourceFile.txt diff --git a/src/MarkdownSnippets/Processing/DirectoryMarkdownProcessor.cs b/src/MarkdownSnippets/Processing/DirectoryMarkdownProcessor.cs index d3e58d86..b1da4e48 100644 --- a/src/MarkdownSnippets/Processing/DirectoryMarkdownProcessor.cs +++ b/src/MarkdownSnippets/Processing/DirectoryMarkdownProcessor.cs @@ -340,7 +340,7 @@ void WriteLines(string target, List lines) // and reduces the chance of colliding with another process reading the file // (e.g. a parallel build packing a nuget that consumes the same markdown). if (File.Exists(target) && - File.ReadAllText(target) == content) + ReadAllTextWithRetry(target) == content) { return; } @@ -348,6 +348,28 @@ void WriteLines(string target, List lines) WriteAllTextWithRetry(target, content); } + static string? ReadAllTextWithRetry(string target) + { + const int maxAttempts = 5; + for (var attempt = 1; ; attempt++) + { + try + { + return File.ReadAllText(target); + } + catch (IOException) when (attempt < maxAttempts) + { + // Another process is writing the file. Back off briefly and retry. + Thread.Sleep(100 * attempt); + } + catch (IOException) + { + // Still locked: treat as changed and let the write path deal with it + return null; + } + } + } + static void WriteAllTextWithRetry(string target, string content) { const int maxAttempts = 5; diff --git a/src/Tests/DirectoryMarkdownProcessor/FileSnippetExplicitInclude/one.source.md b/src/Tests/DirectoryMarkdownProcessor/FileSnippetExplicitInclude/one.source.md new file mode 100644 index 00000000..f189aa67 --- /dev/null +++ b/src/Tests/DirectoryMarkdownProcessor/FileSnippetExplicitInclude/one.source.md @@ -0,0 +1,12 @@ +Local + +snippet: sourceFile.txt + +Relative Local + +snippet: ./sourceFile.txt + +Rooted + +snippet: /sourceFile.txt + diff --git a/src/Tests/DirectoryMarkdownProcessor/FileSnippetExplicitInclude/sourceFile.txt b/src/Tests/DirectoryMarkdownProcessor/FileSnippetExplicitInclude/sourceFile.txt new file mode 100644 index 00000000..3dd3940a --- /dev/null +++ b/src/Tests/DirectoryMarkdownProcessor/FileSnippetExplicitInclude/sourceFile.txt @@ -0,0 +1 @@ +From Source File \ No newline at end of file diff --git a/src/Tests/DirectoryMarkdownProcessorTests.cs b/src/Tests/DirectoryMarkdownProcessorTests.cs index da4a1d6e..de257468 100644 --- a/src/Tests/DirectoryMarkdownProcessorTests.cs +++ b/src/Tests/DirectoryMarkdownProcessorTests.cs @@ -391,7 +391,8 @@ public Task FileSnippetExplicitIncludeBypassesExcludeSnippetFiles() // `begin-snippet`/`end-snippet` markers, but an explicit `snippet: sourceFile.txt` // in a markdown file must still resolve to the whole-file contents — that lookup // goes through allFiles, which remains unfiltered. - var root = Path.GetFullPath("DirectoryMarkdownProcessor/FileSnippet"); + // Own copy of the FileSnippet fixture, so it does not race FileSnippet writing the same one.md + var root = Path.GetFullPath("DirectoryMarkdownProcessor/FileSnippetExplicitInclude"); var processor = new DirectoryMarkdownProcessor( root, writeHeader: false,