From 20180d39e62e5aaf73bf627d4ccd9df06d2d1393 Mon Sep 17 00:00:00 2001 From: KnifeLemon Date: Sat, 24 Oct 2026 01:49:11 +0900 Subject: [PATCH] Make the download cancel test deterministic The test waited up to 10 s on wall-clock timers: for the server to start answering, then for the partial file to appear. On a busy Windows runner the first wait timed out (run 39965468348). The second never really held either: the server sent 16 bytes, fewer than the 64 the downloader sniffs before it creates the file, so WaitUntil ran out its deadline silently and the test cancelled before any partial file existed. Now the server sends a first chunk past the sniffed head and then blocks until the test is over. The test cancels on the first progress report, which comes right after the file is created and written to, checks the file is there, and after the cancellation reaches it checks the file is gone; the downloader deletes it before rethrowing, so no polling is needed. A download that fails before writing fails the test with its own reason instead of a timeout. Co-Authored-By: Claude Opus 5.5 --- tests/Filee.Core.Tests/WebDownloaderTests.cs | 26 ++++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/Filee.Core.Tests/WebDownloaderTests.cs b/tests/Filee.Core.Tests/WebDownloaderTests.cs index c81482915..9c594e392 100644 --- a/tests/Filee.Core.Tests/WebDownloaderTests.cs +++ b/tests/Filee.Core.Tests/WebDownloaderTests.cs @@ -220,27 +220,29 @@ public async Task Gives_up_on_a_server_that_doesnt_answer_or_stalls() [Fact] public async Task Cancelling_stops_the_download_and_removes_the_partial_file() { - var started = new TaskCompletionSource(); + // A first chunk past the sniffed head, so the file gets created, then nothing until the test is over. + var chunk = new byte[WebFormats.SniffLength * 4]; + Png.CopyTo(chunk, 0); _routes["/slow.png"] = async context => { context.Response.ContentType = "image/png"; context.Response.ContentLength64 = 10_000; - await context.Response.OutputStream.WriteAsync(Png); + await context.Response.OutputStream.WriteAsync(chunk); await context.Response.OutputStream.FlushAsync(); - started.TrySetResult(); - await Task.Delay(TimeSpan.FromSeconds(5), _stop.Token); + await Task.Delay(Timeout.Infinite, _stop.Token); }; using var downloader = Downloader(); using var cancel = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken); - var progress = new List(); + // The first report comes right after the file is created and the head written into it. + var written = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var download = downloader.DownloadAsync(Url("/slow.png"), _dir.Path, null, new SyncProgress(progress), cancel.Token); - await started.Task.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken); - await WaitUntil(() => Directory.GetFiles(_dir.Path).Length == 1); + var download = downloader.DownloadAsync(Url("/slow.png"), _dir.Path, null, new ProgressCallback(_ => written.TrySetResult()), cancel.Token); + await await Task.WhenAny(written.Task, download); // a download that fails before writing fails the test with its reason + Assert.Single(Directory.GetFiles(_dir.Path)); cancel.Cancel(); await Assert.ThrowsAnyAsync(() => download); - Assert.Empty(Directory.GetFiles(_dir.Path)); + Assert.Empty(Directory.GetFiles(_dir.Path)); // deleted before the cancellation reaches the caller } [Fact] @@ -295,11 +297,9 @@ public async Task Bytes_a_browser_handed_over_are_saved_like_a_download() program.SaveAsync(_dir.Path, downloader, null, TestContext.Current.CancellationToken))).ErrorKey); } - private static async Task WaitUntil(Func condition) + private sealed class ProgressCallback(Action report) : IProgress { - var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(10); - while (!condition() && DateTime.UtcNow < deadline) - await Task.Delay(10); + public void Report(TransferProgress value) => report(value); } private sealed class SyncProgress(List values) : IProgress