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);
}