From 16e321176b3e880b72a2acf5ac9bac61dee3c182 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 18 Jul 2026 09:26:20 -0700 Subject: [PATCH] Remove repo-wide analyzer relaxation and honor test cancellation CODESTYLE "Analyzer Suppressions" forbids relaxing a batch of rules repo-wide, and names dotnet_analyzer_diagnostic.severity as the exact anti-pattern. Removing it surfaced one rule, xUnit1051, across 29 call sites in the test project; the library and Sandbox were already clean under AnalysisLevel=latest-all with no findings behind the relaxation. Pass TestContext.Current.CancellationToken at each site so xUnit v3 can cancel a stalled test. This matters most in the network-bound Download tests, where a hung request previously had no cancellation path. Every remaining suppression was re-verified as load-bearing by removing it and rebuilding: CA1711 (Ex suffix), CA1707 and CA1515 (xUnit naming and visibility), and the IL3058 NoWarn (Polly and Serilog are not annotated AOT-compatible, and IL3058 has no source location to scope). IDE0055 stays because CSharpier owns formatting; it now carries that rationale as a comment. Co-Authored-By: Claude Opus 4.8 (1M context) --- .editorconfig | 2 +- UtilitiesTests/DownloadAsyncTests.cs | 21 +++++-- UtilitiesTests/ExtensionsTests.cs | 20 ++++-- UtilitiesTests/FileExAsyncTests.cs | 62 +++++++++++++++---- .../HttpClientFactoryResilienceTests.cs | 15 ++++- UtilitiesTests/StringCompressionAsyncTests.cs | 22 +++++-- 6 files changed, 112 insertions(+), 30 deletions(-) diff --git a/.editorconfig b/.editorconfig index 9c7fecc..a392150 100644 --- a/.editorconfig +++ b/.editorconfig @@ -72,8 +72,8 @@ end_of_line = crlf # Suppressions follow CODESTYLE.md "Analyzer Diagnostics and Suppressions": prefer a # [SuppressMessage] attribute or the owning project's .editorconfig; relax a rule # repo-wide here only when it applies to every project (never a brownfield batch). +# IDE0055: CSharpier owns formatting for every project; a second formatter would fight it. dotnet_diagnostic.IDE0055.severity = none -dotnet_analyzer_diagnostic.severity = suggestion csharp_indent_block_contents = true csharp_indent_braces = false csharp_indent_case_contents = true diff --git a/UtilitiesTests/DownloadAsyncTests.cs b/UtilitiesTests/DownloadAsyncTests.cs index 7e74294..9520bfc 100644 --- a/UtilitiesTests/DownloadAsyncTests.cs +++ b/UtilitiesTests/DownloadAsyncTests.cs @@ -9,7 +9,10 @@ public async Task GetContentInfoAsync_WithValidUri_ShouldReturnSuccess() "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png" ); - (bool success, long size, DateTime _) = await Download.GetContentInfoAsync(uri); + (bool success, long size, DateTime _) = await Download.GetContentInfoAsync( + uri, + TestContext.Current.CancellationToken + ); _ = success.Should().BeTrue(); _ = (size > 0).Should().BeTrue(); @@ -20,7 +23,10 @@ public async Task DownloadStringAsync_WithValidUri_ShouldReturnContent() { Uri uri = new("https://www.google.com"); - (bool success, string? content) = await Download.DownloadStringAsync(uri); + (bool success, string? content) = await Download.DownloadStringAsync( + uri, + TestContext.Current.CancellationToken + ); _ = success.Should().BeTrue(); _ = content.Should().NotBeEmpty(); @@ -37,7 +43,11 @@ public async Task DownloadFileAsync_WithValidUri_ShouldCreateFile() try { - bool result = await Download.DownloadFileAsync(uri, tempFile); + bool result = await Download.DownloadFileAsync( + uri, + tempFile, + TestContext.Current.CancellationToken + ); _ = result.Should().BeTrue(); _ = File.Exists(tempFile).Should().BeTrue(); @@ -57,7 +67,10 @@ public async Task DownloadAsync_WithInvalidUri_ShouldReturnFalse() { Uri invalidUri = new("https://thisdoesnotexist123456789.com/file.txt"); - (bool success, long _, DateTime _) = await Download.GetContentInfoAsync(invalidUri); + (bool success, long _, DateTime _) = await Download.GetContentInfoAsync( + invalidUri, + TestContext.Current.CancellationToken + ); _ = success.Should().BeFalse(); } diff --git a/UtilitiesTests/ExtensionsTests.cs b/UtilitiesTests/ExtensionsTests.cs index aa7601f..63e61d8 100644 --- a/UtilitiesTests/ExtensionsTests.cs +++ b/UtilitiesTests/ExtensionsTests.cs @@ -50,7 +50,9 @@ public async Task StringExtension_CompressAsync_ShouldCompressString() { string original = "This is a test string for async compression."; - string compressed = await original.CompressAsync(); + string compressed = await original.CompressAsync( + cancellationToken: TestContext.Current.CancellationToken + ); _ = compressed.Should().NotBeNull(); _ = compressed.Should().NotBeEmpty(); @@ -61,9 +63,13 @@ public async Task StringExtension_CompressAsync_ShouldCompressString() public async Task StringExtension_DecompressAsync_ShouldDecompressString() { string original = "This is a test string for async decompression."; - string compressed = await original.CompressAsync(); + string compressed = await original.CompressAsync( + cancellationToken: TestContext.Current.CancellationToken + ); - string decompressed = await compressed.DecompressAsync(); + string decompressed = await compressed.DecompressAsync( + TestContext.Current.CancellationToken + ); _ = decompressed.Should().Be(original); } @@ -266,8 +272,12 @@ public async Task StringExtension_CompressDecompressAsync_RoundTrip_ShouldPreser foreach (string original in testStrings) { - string compressed = await original.CompressAsync(); - string decompressed = await compressed.DecompressAsync(); + string compressed = await original.CompressAsync( + cancellationToken: TestContext.Current.CancellationToken + ); + string decompressed = await compressed.DecompressAsync( + TestContext.Current.CancellationToken + ); _ = decompressed.Should().Be(original); } diff --git a/UtilitiesTests/FileExAsyncTests.cs b/UtilitiesTests/FileExAsyncTests.cs index ee88841..e826120 100644 --- a/UtilitiesTests/FileExAsyncTests.cs +++ b/UtilitiesTests/FileExAsyncTests.cs @@ -9,7 +9,10 @@ public async Task DeleteFileAsync_WithExistingFile_ShouldReturnTrue() try { - bool result = await FileEx.DeleteFileAsync(tempFile); + bool result = await FileEx.DeleteFileAsync( + tempFile, + TestContext.Current.CancellationToken + ); _ = result.Should().BeTrue(); _ = File.Exists(tempFile).Should().BeFalse(); @@ -31,7 +34,10 @@ public async Task DeleteDirectoryAsync_WithExistingDirectory_ShouldReturnTrue() try { - bool result = await FileEx.DeleteDirectoryAsync(tempDir); + bool result = await FileEx.DeleteDirectoryAsync( + tempDir, + TestContext.Current.CancellationToken + ); _ = result.Should().BeTrue(); _ = Directory.Exists(tempDir).Should().BeFalse(); @@ -53,11 +59,19 @@ public async Task DeleteDirectoryAsync_Recursive_ShouldDeleteAllContents() string subDir = Path.Combine(tempDir, "subdir"); _ = Directory.CreateDirectory(subDir); string testFile = Path.Combine(subDir, "test.txt"); - await File.WriteAllTextAsync(testFile, "test content"); + await File.WriteAllTextAsync( + testFile, + "test content", + TestContext.Current.CancellationToken + ); try { - bool result = await FileEx.DeleteDirectoryAsync(tempDir, recursive: true); + bool result = await FileEx.DeleteDirectoryAsync( + tempDir, + recursive: true, + TestContext.Current.CancellationToken + ); _ = result.Should().BeTrue(); _ = Directory.Exists(tempDir).Should().BeFalse(); @@ -79,7 +93,11 @@ public async Task RenameFileAsync_WithValidPaths_ShouldRenameFile() try { - bool result = await FileEx.RenameFileAsync(tempFile, newPath); + bool result = await FileEx.RenameFileAsync( + tempFile, + newPath, + TestContext.Current.CancellationToken + ); _ = result.Should().BeTrue(); _ = File.Exists(tempFile).Should().BeFalse(); @@ -107,7 +125,11 @@ public async Task RenameFolderAsync_WithValidPaths_ShouldRenameFolder() try { - bool result = await FileEx.RenameFolderAsync(tempDir, newPath); + bool result = await FileEx.RenameFolderAsync( + tempDir, + newPath, + TestContext.Current.CancellationToken + ); _ = result.Should().BeTrue(); _ = Directory.Exists(tempDir).Should().BeFalse(); @@ -133,9 +155,16 @@ public async Task WaitFileReadableAsync_WithReadableFile_ShouldReturnTrue() try { - await File.WriteAllTextAsync(tempFile, "test content"); + await File.WriteAllTextAsync( + tempFile, + "test content", + TestContext.Current.CancellationToken + ); - bool result = await FileEx.WaitFileReadableAsync(tempFile); + bool result = await FileEx.WaitFileReadableAsync( + tempFile, + TestContext.Current.CancellationToken + ); _ = result.Should().BeTrue(); } @@ -156,7 +185,11 @@ public async Task CreateRandomFilledFileAsync_ShouldCreateFile() try { - bool result = await FileEx.CreateRandomFilledFileAsync(tempFile, fileSize); + bool result = await FileEx.CreateRandomFilledFileAsync( + tempFile, + fileSize, + TestContext.Current.CancellationToken + ); _ = result.Should().BeTrue(); _ = File.Exists(tempFile).Should().BeTrue(); @@ -224,11 +257,18 @@ public async Task DeleteInsideDirectoryAsync_ShouldDeleteContentsOnly() string tempDir = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}"); _ = Directory.CreateDirectory(tempDir); string testFile = Path.Combine(tempDir, "test.txt"); - await File.WriteAllTextAsync(testFile, "test content"); + await File.WriteAllTextAsync( + testFile, + "test content", + TestContext.Current.CancellationToken + ); try { - bool result = await FileEx.DeleteInsideDirectoryAsync(tempDir); + bool result = await FileEx.DeleteInsideDirectoryAsync( + tempDir, + TestContext.Current.CancellationToken + ); _ = result.Should().BeTrue(); _ = Directory.Exists(tempDir).Should().BeTrue(); // Directory should still exist diff --git a/UtilitiesTests/HttpClientFactoryResilienceTests.cs b/UtilitiesTests/HttpClientFactoryResilienceTests.cs index 4d76b1b..119e2e8 100644 --- a/UtilitiesTests/HttpClientFactoryResilienceTests.cs +++ b/UtilitiesTests/HttpClientFactoryResilienceTests.cs @@ -14,7 +14,10 @@ public async Task TransientStatus_IsRetriedThenSucceeds() ); using HttpClient client = CreateStubbedClient(stub, FastOptions()); - using HttpResponseMessage response = await client.GetAsync(new Uri("http://localhost/")); + using HttpResponseMessage response = await client.GetAsync( + new Uri("http://localhost/"), + TestContext.Current.CancellationToken + ); _ = response.StatusCode.Should().Be(HttpStatusCode.OK); _ = stub.CallCount.Should().Be(3); // initial attempt + 2 retries @@ -26,7 +29,10 @@ public async Task NonTransientStatus_IsNotRetried() using StubHttpMessageHandler stub = new(Status(HttpStatusCode.NotFound)); using HttpClient client = CreateStubbedClient(stub, FastOptions()); - using HttpResponseMessage response = await client.GetAsync(new Uri("http://localhost/")); + using HttpResponseMessage response = await client.GetAsync( + new Uri("http://localhost/"), + TestContext.Current.CancellationToken + ); _ = response.StatusCode.Should().Be(HttpStatusCode.NotFound); _ = stub.CallCount.Should().Be(1); // 404 is not transient @@ -123,7 +129,10 @@ public async Task HttpRequestExceptionWithTransientStatus_IsRetriedThenSucceeds( ); using HttpClient client = CreateStubbedClient(stub, FastOptions()); - using HttpResponseMessage response = await client.GetAsync(new Uri("http://localhost/")); + using HttpResponseMessage response = await client.GetAsync( + new Uri("http://localhost/"), + TestContext.Current.CancellationToken + ); _ = response.StatusCode.Should().Be(HttpStatusCode.OK); _ = stub.CallCount.Should().Be(3); // 503 exception retried, then success diff --git a/UtilitiesTests/StringCompressionAsyncTests.cs b/UtilitiesTests/StringCompressionAsyncTests.cs index ea160ee..edadee6 100644 --- a/UtilitiesTests/StringCompressionAsyncTests.cs +++ b/UtilitiesTests/StringCompressionAsyncTests.cs @@ -14,10 +14,14 @@ public async Task CompressDecompressAsync() string text = Convert.ToBase64String(buffer); // Compress the string asynchronously - string compressed = await text.CompressAsync(); + string compressed = await text.CompressAsync( + cancellationToken: TestContext.Current.CancellationToken + ); // Decompress asynchronously - string decompressed = await compressed.DecompressAsync(); + string decompressed = await compressed.DecompressAsync( + TestContext.Current.CancellationToken + ); // Compare to original string _ = decompressed.Should().Be(text); @@ -32,8 +36,10 @@ public async Task CompressAsync_WithDifferentLevels_ShouldSucceed(CompressionLev string text = "This is a test string that will be compressed with different compression levels."; - string compressed = await text.CompressAsync(level); - string decompressed = await compressed.DecompressAsync(); + string compressed = await text.CompressAsync(level, TestContext.Current.CancellationToken); + string decompressed = await compressed.DecompressAsync( + TestContext.Current.CancellationToken + ); _ = decompressed.Should().Be(text); } @@ -91,8 +97,12 @@ public async Task CompressAsync_LargeString_ShouldCompress() // Create a large repetitive string (should compress well) string largeText = new('A', 1024 * 1024); // 1MB of 'A's - string compressed = await largeText.CompressAsync(); - string decompressed = await compressed.DecompressAsync(); + string compressed = await largeText.CompressAsync( + cancellationToken: TestContext.Current.CancellationToken + ); + string decompressed = await compressed.DecompressAsync( + TestContext.Current.CancellationToken + ); _ = decompressed.Should().Be(largeText); _ = (compressed.Length < largeText.Length)