Skip to content

Commit 3c9f486

Browse files
author
Pavol Beťák
committed
Added RETRY-UNTIL-TEST-PASS directive
1 parent 5db90a4 commit 3c9f486

27 files changed

Lines changed: 408 additions & 125 deletions
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Initialize counters for different retry scenarios
2+
tp.SetVariable("CarAvailabilityCheckCount", 0);
3+
tp.SetVariable("UnavailableCarCheckCount", 0);
4+
tp.SetVariable("CarCreationCheckCount", 0);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
### Scenario 1: Check Car Availability (passes after 3 attempts)
2+
## RETRY-STRATEGY: Custom retry
3+
## RETRY-MAX-ATTEMPTS: 5
4+
## RETRY-UNTIL-TEST-PASS: Car should be available
5+
6+
# @name CheckCarAvailabilityRequest
7+
GET {{ApiBaseUrl}}{{ApiCarsSection}}/1
8+
9+
###
10+
11+
### Scenario 2: Check Unavailable Car (never passes - exceeds max attempts)
12+
## RETRY-STRATEGY: Custom retry
13+
## RETRY-MAX-ATTEMPTS: 3
14+
## RETRY-UNTIL-TEST-PASS: Car with special features should exist
15+
16+
# @name CheckUnavailableCarRequest
17+
GET {{ApiBaseUrl}}{{ApiCarsSection}}/999
18+
19+
###
20+
21+
### Scenario 3: Verify Car Creation (combined RETRY-UNTIL-STATUS + RETRY-UNTIL-TEST-PASS)
22+
## RETRY-STRATEGY: Custom retry
23+
## RETRY-MAX-ATTEMPTS: 4
24+
## RETRY-MAX-DELAY: 00:00:02
25+
## RETRY-UNTIL-STATUS: [200, 201]
26+
## RETRY-UNTIL-TEST-PASS: Newly created car should have correct brand
27+
28+
# @name VerifyCarCreationRequest
29+
GET {{ApiBaseUrl}}{{ApiCarsSection}}/7
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Test 1: Car becomes available after 3 attempts
2+
tp.Test("Car should be available", () =>
3+
{
4+
var checkCount = tp.GetVariable<int>("CarAvailabilityCheckCount");
5+
checkCount++;
6+
tp.SetVariable("CarAvailabilityCheckCount", checkCount);
7+
8+
var response = tp.Response;
9+
var statusCode = response.StatusCode();
10+
11+
if (checkCount >= 3)
12+
{
13+
tp.Logger.LogInformation($"Car is now available after {checkCount} attempts!");
14+
Equal(200, statusCode);
15+
}
16+
else
17+
{
18+
tp.Logger.LogInformation($"Car not available yet (attempt {checkCount})");
19+
throw new Exception($"Car not available yet (attempt {checkCount})");
20+
}
21+
});
22+
23+
// Test 2: Car never becomes available (will fail after max attempts)
24+
await tp.Test("Car with special features should exist", async () =>
25+
{
26+
var checkCount = tp.GetVariable<int>("UnavailableCarCheckCount");
27+
checkCount++;
28+
tp.SetVariable("UnavailableCarCheckCount", checkCount);
29+
30+
var response = tp.Response;
31+
var statusCode = response.StatusCode();
32+
33+
tp.Logger.LogInformation($"Checking for unavailable car (attempt {checkCount}), status: {statusCode}");
34+
35+
// This will always fail - demonstrating max attempts behavior
36+
Equal(200, statusCode);
37+
38+
dynamic carData = await response.GetBodyAsExpandoAsync();
39+
Equal("SpecialFeatures", carData.Brand);
40+
});
41+
42+
// Test 3: Combined scenario - verify newly created car has correct brand
43+
await tp.Test("Newly created car should have correct brand", async () =>
44+
{
45+
var checkCount = tp.GetVariable<int>("CarCreationCheckCount");
46+
checkCount++;
47+
tp.SetVariable("CarCreationCheckCount", checkCount);
48+
49+
var response = tp.Response;
50+
var statusCode = response.StatusCode();
51+
52+
tp.Logger.LogInformation($"Verifying car creation (attempt {checkCount}), status: {statusCode}");
53+
54+
if (checkCount >= 2 && (statusCode == 200 || statusCode == 201))
55+
{
56+
dynamic carData = await response.GetBodyAsExpandoAsync();
57+
tp.Logger.LogInformation($"Car found with brand: {carData.Brand}");
58+
Equal("Audi", carData.Brand);
59+
}
60+
else
61+
{
62+
tp.Logger.LogInformation($"Car not ready yet (attempt {checkCount})");
63+
throw new Exception($"Car not ready yet (attempt {checkCount})");
64+
}
65+
});

demo/Tests/003-Car-Rentals/001-Rent-Car-test.csx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,4 @@ await tp.Test($"Rented car should be '{car}'.", async () =>
3636
Equal(car.Year, retrievedCar.Year);
3737
});
3838

39-
ClearVariables();
40-
4139
tp.Logger.LogInformation("End of demo collection testing.");

src/TeaPie/ApplicationBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ private static TeaPie CreateUserContext(IServiceProvider provider, ApplicationCo
165165
provider.GetRequiredService<IVariables>(),
166166
provider.GetRequiredService<IFunctions>(),
167167
provider.GetRequiredService<ILogger<TeaPie>>(),
168-
provider.GetRequiredService<ITester>(),
168+
provider.GetRequiredService<IRegistrator>(),
169169
provider.GetRequiredService<ICurrentTestCaseExecutionContextAccessor>(),
170170
provider.GetRequiredService<ITestResultsSummaryReporter>(),
171171
provider.GetRequiredService<IRetryStrategyRegistry>(),

src/TeaPie/Http/Parsing/DirectivesLineParser.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ internal class DirectivesLineParser : ILineParser
1010
new TestDirectivesLineParser(),
1111
new RetryStrategyDirectiveLineParser(),
1212
new RetryUntilStatusCodesLineParser(),
13+
new RetryUntilTestPassDirectiveLineParser(),
1314
new RetryExplicitPropertiesDirectiveLineParser()
1415
];
1516

src/TeaPie/Http/Parsing/HttpParsingContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ internal class HttpParsingContext(HttpRequestHeaders defaultHeaders)
2626
public string RetryStrategyName { get; set; } = string.Empty;
2727
public IReadOnlyList<HttpStatusCode> RetryUntilStatusCodes { get; set; } = [];
2828
public RetryStrategyOptions<HttpResponseMessage>? ExplicitRetryStrategy { get; set; }
29+
public string RetryUntilTestPassTestName { get; set; } = string.Empty;
2930

3031
public string AuthProviderName { get; set; } = string.Empty;
3132

src/TeaPie/Http/Parsing/HttpRequestParser.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ private void UpdateResiliencePipeline(RequestExecutionContext requestExecutionCo
131131
_resiliencePipelineProvider.GetResiliencePipeline(
132132
parsingContext.RetryStrategyName,
133133
parsingContext.ExplicitRetryStrategy,
134-
parsingContext.RetryUntilStatusCodes);
134+
parsingContext.RetryUntilStatusCodes,
135+
parsingContext.RetryUntilTestPassTestName);
135136

136137
private static void CreateMessageContent(HttpParsingContext context, HttpRequestMessage requestMessage)
137138
{

src/TeaPie/Http/Retrying/ResiliencePipelineProvider.cs

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Polly.Retry;
44
using System.Net;
55
using System.Text;
6+
using TeaPie.Testing;
67
using ResiliencePipeline = Polly.ResiliencePipeline<System.Net.Http.HttpResponseMessage>;
78
using RetryStrategy = Polly.Retry.RetryStrategyOptions<System.Net.Http.HttpResponseMessage>;
89

@@ -11,14 +12,21 @@ namespace TeaPie.Http.Retrying;
1112
internal interface IResiliencePipelineProvider
1213
{
1314
ResiliencePipeline GetResiliencePipeline(
14-
string nameOfBaseStrategy, RetryStrategy? explicitlyOverridenStrategy, IReadOnlyList<HttpStatusCode> statusCodes);
15+
string nameOfBaseStrategy,
16+
RetryStrategy? explicitlyOverridenStrategy,
17+
IReadOnlyList<HttpStatusCode> statusCodes,
18+
string? retryUntilTestPassTestName = null);
1519
}
1620

17-
internal class ResiliencePipelineProvider(IRetryStrategyRegistry registry, ILogger<ResiliencePipelineProvider> logger)
21+
internal class ResiliencePipelineProvider(
22+
IRetryStrategyRegistry registry,
23+
ILogger<ResiliencePipelineProvider> logger,
24+
ITester testExecutor)
1825
: IResiliencePipelineProvider
1926
{
2027
private readonly IRetryStrategyRegistry _retryStrategyRegistry = registry;
2128
private readonly ILogger<ResiliencePipelineProvider> _logger = logger;
29+
private readonly ITester _testExecutor = testExecutor;
2230

2331
public static readonly ResiliencePipeline DefaultResiliencePipeline =
2432
ResiliencePipeline.Empty;
@@ -27,14 +35,19 @@ internal class ResiliencePipelineProvider(IRetryStrategyRegistry registry, ILogg
2735
new() { { string.Empty, DefaultResiliencePipeline } };
2836

2937
public ResiliencePipeline<HttpResponseMessage> GetResiliencePipeline(
30-
string nameOfBaseRetryStrategy, RetryStrategy? overridRetryStrategy, IReadOnlyList<HttpStatusCode> statusCodes)
38+
string nameOfBaseRetryStrategy,
39+
RetryStrategy? overridRetryStrategy,
40+
IReadOnlyList<HttpStatusCode> statusCodes,
41+
string? retryUntilTestPassTestName = null)
3142
{
3243
var isBaseRetryStrategyAltered = false;
3344
CheckAndResolveBaseRetryStrategy(nameOfBaseRetryStrategy, out var finalRetryStrategy, out var nameOfFinalStrategy);
3445
ApplyExplicitOverridesIfAny(
3546
overridRetryStrategy, ref finalRetryStrategy, ref nameOfFinalStrategy, ref isBaseRetryStrategyAltered);
3647
ApplyRetryUntilStatusCodesConditionIfAny(
3748
statusCodes, ref finalRetryStrategy, ref nameOfFinalStrategy, ref isBaseRetryStrategyAltered);
49+
ApplyRetryUntilTestPassConditionIfAny(
50+
retryUntilTestPassTestName, ref finalRetryStrategy, ref nameOfFinalStrategy, ref isBaseRetryStrategyAltered);
3851

3952
LogUsageOfRetryStrategy(nameOfFinalStrategy, finalRetryStrategy, isBaseRetryStrategyAltered);
4053

@@ -50,7 +63,25 @@ private void ApplyRetryUntilStatusCodesConditionIfAny(
5063
if (statusCodes.Any())
5164
{
5265
nameOfFinalStrategy = GetRetryUntilStatusCodesStrategyName(statusCodes, nameOfFinalStrategy);
53-
finalRetryStrategy = GetRetryStrategy(statusCodes, finalRetryStrategy);
66+
finalRetryStrategy = GetRetryStrategyWithCondition(
67+
response => !statusCodes.Contains(response.StatusCode),
68+
finalRetryStrategy);
69+
altered = true;
70+
}
71+
}
72+
73+
private void ApplyRetryUntilTestPassConditionIfAny(
74+
string? testName,
75+
ref RetryStrategy finalRetryStrategy,
76+
ref string nameOfFinalStrategy,
77+
ref bool altered)
78+
{
79+
if (!string.IsNullOrEmpty(testName))
80+
{
81+
nameOfFinalStrategy = $"{nameOfFinalStrategy}+{RetryingDirectives.RetryUntilTestPassDirectiveFullName}:{testName}";
82+
finalRetryStrategy = GetRetryStrategyWithCondition(
83+
response => !_testExecutor.ExecuteTestSync(testName, response),
84+
finalRetryStrategy);
5485
altered = true;
5586
}
5687
}
@@ -120,12 +151,14 @@ private static ResiliencePipeline BuildPipeline(RetryStrategy retryStrategy)
120151
.AddRetry(retryStrategy)
121152
.Build();
122153

123-
private static RetryStrategy GetRetryStrategy(IReadOnlyList<HttpStatusCode> statusCodes, RetryStrategy baseRetryStrategy)
154+
private static RetryStrategy GetRetryStrategyWithCondition(
155+
Func<HttpResponseMessage, bool> shouldRetryPredicate,
156+
RetryStrategy baseRetryStrategy)
124157
{
125158
var retryStrategyWithCondition = new RetryStrategy()
126159
{
127160
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
128-
.HandleResult(response => !statusCodes.Contains(response.StatusCode))
161+
.HandleResult(shouldRetryPredicate)
129162
};
130163

131164
return MergeRetryStrategies(baseRetryStrategy, retryStrategyWithCondition);
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System.Text.RegularExpressions;
2+
using TeaPie.Http.Parsing;
3+
4+
namespace TeaPie.Http.Retrying;
5+
6+
internal partial class RetryUntilTestPassDirectiveLineParser : ILineParser
7+
{
8+
public bool CanParse(string line, HttpParsingContext context)
9+
=> Regex.IsMatch(line, RetryingDirectives.RetryUntilTestPassDirectivePattern);
10+
11+
public void Parse(string line, HttpParsingContext context)
12+
{
13+
var match = Regex.Match(line, RetryingDirectives.RetryUntilTestPassDirectivePattern);
14+
if (match.Success)
15+
{
16+
var testName = match.Groups[RetryingDirectives.RetryUntilTestPassDirectiveParameterName].Value;
17+
context.RetryUntilTestPassTestName = testName.Trim();
18+
}
19+
else
20+
{
21+
throw new InvalidOperationException(
22+
$"Unable to parse '{RetryingDirectives.RetryUntilTestPassDirectiveFullName}' " +
23+
"if directive doesn't match the structure.");
24+
}
25+
}
26+
}

0 commit comments

Comments
 (0)