From 95584e0d9ba89013f63da0158bfa3492d5be8739 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 25 Apr 2026 00:22:17 +0000 Subject: [PATCH 1/3] Add unit tests for RetryAttribute Add 13 unit tests covering RetryAttribute constructor validation, BackoffType property validation, and ExecuteAsync retry logic: - Constructor rejects maxRetryAttempts < 1 - BackoffType setter rejects invalid enum values - ExecuteAsync stops early when retry succeeds - ExecuteAsync runs exactly MaxRetryAttempts times on all failures - ExecuteAsync stops on Inconclusive result Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Attributes/RetryAttributeTests.cs | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 test/UnitTests/TestFramework.UnitTests/Attributes/RetryAttributeTests.cs diff --git a/test/UnitTests/TestFramework.UnitTests/Attributes/RetryAttributeTests.cs b/test/UnitTests/TestFramework.UnitTests/Attributes/RetryAttributeTests.cs new file mode 100644 index 0000000000..f775b00b97 --- /dev/null +++ b/test/UnitTests/TestFramework.UnitTests/Attributes/RetryAttributeTests.cs @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#pragma warning disable MSTESTEXP // Experimental API + +using AwesomeAssertions; + +using TestFramework.ForTestingMSTest; + +namespace UnitTestFramework.Tests; + +/// +/// Tests for constructor validation and retry execution logic. +/// +public class RetryAttributeTests : TestContainer +{ + public void Constructor_WhenMaxRetryAttemptsIsZero_ThrowsArgumentOutOfRangeException() + { + Action act = static () => _ = new RetryAttribute(0); + act.Should().Throw() + .WithParameterName("maxRetryAttempts"); + } + + public void Constructor_WhenMaxRetryAttemptsIsNegative_ThrowsArgumentOutOfRangeException() + { + Action act = static () => _ = new RetryAttribute(-1); + act.Should().Throw() + .WithParameterName("maxRetryAttempts"); + } + + public void Constructor_WhenMaxRetryAttemptsIsOne_SetsMaxRetryAttempts() + { + var attribute = new RetryAttribute(1); + attribute.MaxRetryAttempts.Should().Be(1); + } + + public void Constructor_WhenMaxRetryAttemptsIsPositive_SetsMaxRetryAttempts() + { + var attribute = new RetryAttribute(5); + attribute.MaxRetryAttempts.Should().Be(5); + } + + public void BackoffType_DefaultsToConstant() + { + var attribute = new RetryAttribute(2); + attribute.BackoffType.Should().Be(DelayBackoffType.Constant); + } + + public void BackoffType_WhenSetToInvalidValue_ThrowsArgumentOutOfRangeException() + { + var attribute = new RetryAttribute(2); + Action act = () => attribute.BackoffType = (DelayBackoffType)99; + act.Should().Throw() + .WithParameterName("value"); + } + + public void BackoffType_WhenSetToExponential_Succeeds() + { + var attribute = new RetryAttribute(2); + attribute.BackoffType = DelayBackoffType.Exponential; + attribute.BackoffType.Should().Be(DelayBackoffType.Exponential); + } + + public void MillisecondsDelayBetweenRetries_DefaultsToZero() + { + var attribute = new RetryAttribute(2); + attribute.MillisecondsDelayBetweenRetries.Should().Be(0); + } + + public async Task ExecuteAsync_WhenTestPassesOnFirstRetry_StopsRetryingEarly() + { + var attribute = new RetryAttribute(maxRetryAttempts: 5); + int callCount = 0; + + var passResult = new TestResult { Outcome = UnitTestOutcome.Passed }; + var failResult = new TestResult { Outcome = UnitTestOutcome.Failed }; + + var firstRunResults = new[] { failResult }; + var context = new RetryContext( + () => + { + callCount++; + return Task.FromResult(new[] { passResult }); + }, + firstRunResults); + + RetryResult result = await attribute.ExecuteAsync(context); + + callCount.Should().Be(1); + result.TryGetLast().Should().ContainSingle() + .Which.Outcome.Should().Be(UnitTestOutcome.Passed); + } + + public async Task ExecuteAsync_WhenAllRetriesFail_ExecutesExactlyMaxRetryAttemptsTimes() + { + var attribute = new RetryAttribute(maxRetryAttempts: 3); + int callCount = 0; + + var failResult = new TestResult { Outcome = UnitTestOutcome.Failed }; + var firstRunResults = new[] { failResult }; + + var context = new RetryContext( + () => + { + callCount++; + return Task.FromResult(new[] { new TestResult { Outcome = UnitTestOutcome.Failed } }); + }, + firstRunResults); + + RetryResult result = await attribute.ExecuteAsync(context); + + callCount.Should().Be(3); + } + + public async Task ExecuteAsync_WhenAllRetriesFail_ResultContainsLastAttemptOutcome() + { + var attribute = new RetryAttribute(maxRetryAttempts: 2); + int callCount = 0; + + var firstRunResults = new[] { new TestResult { Outcome = UnitTestOutcome.Failed } }; + var context = new RetryContext( + () => + { + callCount++; + var outcome = callCount == 2 ? UnitTestOutcome.Timeout : UnitTestOutcome.Failed; + return Task.FromResult(new[] { new TestResult { Outcome = outcome } }); + }, + firstRunResults); + + RetryResult result = await attribute.ExecuteAsync(context); + + result.TryGetLast().Should().ContainSingle() + .Which.Outcome.Should().Be(UnitTestOutcome.Timeout); + } + + public async Task ExecuteAsync_WhenTestPassesAfterSeveralFailures_StopsAtFirstSuccess() + { + var attribute = new RetryAttribute(maxRetryAttempts: 5); + int callCount = 0; + + var firstRunResults = new[] { new TestResult { Outcome = UnitTestOutcome.Failed } }; + var context = new RetryContext( + () => + { + callCount++; + var outcome = callCount < 3 ? UnitTestOutcome.Failed : UnitTestOutcome.Passed; + return Task.FromResult(new[] { new TestResult { Outcome = outcome } }); + }, + firstRunResults); + + RetryResult result = await attribute.ExecuteAsync(context); + + callCount.Should().Be(3); + result.TryGetLast().Should().ContainSingle() + .Which.Outcome.Should().Be(UnitTestOutcome.Passed); + } + + public async Task ExecuteAsync_WhenResultIsInconclusive_StopsRetrying() + { + var attribute = new RetryAttribute(maxRetryAttempts: 5); + int callCount = 0; + + var firstRunResults = new[] { new TestResult { Outcome = UnitTestOutcome.Failed } }; + var context = new RetryContext( + () => + { + callCount++; + return Task.FromResult(new[] { new TestResult { Outcome = UnitTestOutcome.Inconclusive } }); + }, + firstRunResults); + + await attribute.ExecuteAsync(context); + + callCount.Should().Be(1); + } +} From fd3be34209f7248e3557ec53e5b9816e5b78b7f5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 00:27:34 +0000 Subject: [PATCH 2/3] Fix IDE0008/IDE0017 code style violations in RetryAttributeTests Apply object initializer syntax (IDE0017) and explicit types instead of var with non-apparent types (IDE0008) to pass Windows CI build which treats analyzer warnings as errors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Attributes/RetryAttributeTests.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/test/UnitTests/TestFramework.UnitTests/Attributes/RetryAttributeTests.cs b/test/UnitTests/TestFramework.UnitTests/Attributes/RetryAttributeTests.cs index f775b00b97..ca9b34bb85 100644 --- a/test/UnitTests/TestFramework.UnitTests/Attributes/RetryAttributeTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Attributes/RetryAttributeTests.cs @@ -56,8 +56,7 @@ public void BackoffType_WhenSetToInvalidValue_ThrowsArgumentOutOfRangeException( public void BackoffType_WhenSetToExponential_Succeeds() { - var attribute = new RetryAttribute(2); - attribute.BackoffType = DelayBackoffType.Exponential; + RetryAttribute attribute = new(2) { BackoffType = DelayBackoffType.Exponential }; attribute.BackoffType.Should().Be(DelayBackoffType.Exponential); } @@ -75,7 +74,7 @@ public async Task ExecuteAsync_WhenTestPassesOnFirstRetry_StopsRetryingEarly() var passResult = new TestResult { Outcome = UnitTestOutcome.Passed }; var failResult = new TestResult { Outcome = UnitTestOutcome.Failed }; - var firstRunResults = new[] { failResult }; + TestResult[] firstRunResults = [failResult]; var context = new RetryContext( () => { @@ -97,7 +96,7 @@ public async Task ExecuteAsync_WhenAllRetriesFail_ExecutesExactlyMaxRetryAttempt int callCount = 0; var failResult = new TestResult { Outcome = UnitTestOutcome.Failed }; - var firstRunResults = new[] { failResult }; + TestResult[] firstRunResults = [failResult]; var context = new RetryContext( () => @@ -117,12 +116,12 @@ public async Task ExecuteAsync_WhenAllRetriesFail_ResultContainsLastAttemptOutco var attribute = new RetryAttribute(maxRetryAttempts: 2); int callCount = 0; - var firstRunResults = new[] { new TestResult { Outcome = UnitTestOutcome.Failed } }; + TestResult[] firstRunResults = [new TestResult { Outcome = UnitTestOutcome.Failed }]; var context = new RetryContext( () => { callCount++; - var outcome = callCount == 2 ? UnitTestOutcome.Timeout : UnitTestOutcome.Failed; + UnitTestOutcome outcome = callCount == 2 ? UnitTestOutcome.Timeout : UnitTestOutcome.Failed; return Task.FromResult(new[] { new TestResult { Outcome = outcome } }); }, firstRunResults); @@ -138,12 +137,12 @@ public async Task ExecuteAsync_WhenTestPassesAfterSeveralFailures_StopsAtFirstSu var attribute = new RetryAttribute(maxRetryAttempts: 5); int callCount = 0; - var firstRunResults = new[] { new TestResult { Outcome = UnitTestOutcome.Failed } }; + TestResult[] firstRunResults = [new TestResult { Outcome = UnitTestOutcome.Failed }]; var context = new RetryContext( () => { callCount++; - var outcome = callCount < 3 ? UnitTestOutcome.Failed : UnitTestOutcome.Passed; + UnitTestOutcome outcome = callCount < 3 ? UnitTestOutcome.Failed : UnitTestOutcome.Passed; return Task.FromResult(new[] { new TestResult { Outcome = outcome } }); }, firstRunResults); @@ -160,7 +159,7 @@ public async Task ExecuteAsync_WhenResultIsInconclusive_StopsRetrying() var attribute = new RetryAttribute(maxRetryAttempts: 5); int callCount = 0; - var firstRunResults = new[] { new TestResult { Outcome = UnitTestOutcome.Failed } }; + TestResult[] firstRunResults = [new TestResult { Outcome = UnitTestOutcome.Failed }]; var context = new RetryContext( () => { From 8f7e613cd0a10ee2cf2ec9709477ef11a779cbf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Sun, 26 Apr 2026 17:43:02 +0200 Subject: [PATCH 3/3] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../TestFramework.UnitTests/Attributes/RetryAttributeTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/UnitTests/TestFramework.UnitTests/Attributes/RetryAttributeTests.cs b/test/UnitTests/TestFramework.UnitTests/Attributes/RetryAttributeTests.cs index ca9b34bb85..f1706ed0cc 100644 --- a/test/UnitTests/TestFramework.UnitTests/Attributes/RetryAttributeTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Attributes/RetryAttributeTests.cs @@ -7,7 +7,7 @@ using TestFramework.ForTestingMSTest; -namespace UnitTestFramework.Tests; +namespace Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests.Attributes; /// /// Tests for constructor validation and retry execution logic. @@ -106,7 +106,7 @@ public async Task ExecuteAsync_WhenAllRetriesFail_ExecutesExactlyMaxRetryAttempt }, firstRunResults); - RetryResult result = await attribute.ExecuteAsync(context); + await attribute.ExecuteAsync(context); callCount.Should().Be(3); }