Skip to content

Commit 9d2e154

Browse files
authored
Merge branch 'main' into landonxjames/crc-feature
2 parents 1c57b3c + 5bbf0a1 commit 9d2e154

File tree

9 files changed

+374
-33
lines changed

9 files changed

+374
-33
lines changed

.changelog/1747668519.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
applies_to:
3+
- aws-sdk-rust
4+
- client
5+
authors:
6+
- aajtodd
7+
references:
8+
- smithy-rs#4135
9+
breaking: false
10+
new_feature: false
11+
bug_fix: true
12+
---
13+
fix simple rules behavior with `RuleMode::MatchAny`

.changelog/1747668565.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
applies_to:
3+
- client
4+
- aws-sdk-rust
5+
authors:
6+
- aajtodd
7+
references:
8+
- smithy-rs#4135
9+
breaking: false
10+
new_feature: true
11+
bug_fix: false
12+
---
13+
Introduce a new `repeatedly()` function to `aws-smithy-mocks` sequence builder to build mock rules that behave as an
14+
infinite sequence.
15+
16+
```rust
17+
let rule = mock!(aws_sdk_s3::Client::get_object)
18+
.sequence()
19+
.http_status(503, None)
20+
.times(2) // repeat the last output twice before moving onto the next response in the sequence
21+
.output(|| GetObjectOutput::builder()
22+
.body(ByteStream::from_static(b"success"))
23+
.build()
24+
)
25+
.repeatedly() // repeat the last output forever
26+
.build();
27+
```

aws/rust-runtime/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

aws/rust-runtime/aws-config/Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust-runtime/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust-runtime/aws-smithy-mocks/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "aws-smithy-mocks"
3-
version = "0.1.0"
3+
version = "0.1.1"
44
authors = ["AWS Rust SDK Team <aws-sdk-rust@amazon.com>"]
55
description = "Testing utilities for smithy-rs generated clients"
66
edition = "2021"

rust-runtime/aws-smithy-mocks/README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,19 @@ let retry_rule = mock!(Client::get_object)
104104
.times(2) // First two calls return 503
105105
.output(|| GetObjectOutput::builder().build()) // Third call succeeds
106106
.build();
107+
108+
// Repeat a response indefinitely
109+
let infinite_rule = mock!(Client::get_object)
110+
.sequence()
111+
.error(|| GetObjectError::NoSuchKey(NoSuchKey::builder().build()))
112+
.output(|| GetObjectOutput::builder().build()) // Second call succeeds
113+
.repeatedly() // All subsequent calls succeed
114+
.build();
107115
```
108116

117+
The `times(n)` method repeats the last added response `n` times, while `repeatedly()` causes the last response to
118+
repeat indefinitely, making the rule never exhaust.
119+
109120
The sequence builder API provides a fluent interface for defining sequences of responses.
110121
After providing all responses in the sequence, the rule is considered exhausted.
111122

@@ -133,8 +144,15 @@ let client = mock_client!(
133144

134145
The [`RuleMode`] enum controls how rules are matched and applied:
135146

147+
Given a simple (non-sequenced) based rule (e.g. `.then_output()`, `.then_error()`, or `.then_http_response()`):
148+
- `RuleMode::Sequential`: The rule is used once and then the next rule is used.
149+
- `RuleMode::MatchAny`: Rule is used repeatedly as many times as it is matched.
150+
151+
In other words, simple rules behave as single use rules in `Sequential` mode and as infinite sequences in `MatchAny` mode.
152+
153+
Given a sequenced rule (e.g. via `.sequence()`):
136154
- `RuleMode::Sequential`: Rules are tried in order. When a rule is exhausted, the next rule is used.
137-
- `RuleMode::MatchAny`: The first matching rule is used, regardless of order.
155+
- `RuleMode::MatchAny`: The first (non-exhausted) matching rule is used, regardless of order.
138156

139157
```rust,ignore
140158
let interceptor = MockResponseInterceptor::new()

rust-runtime/aws-smithy-mocks/src/interceptor.rs

Lines changed: 184 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,12 @@ impl Intercept for MockResponseInterceptor {
111111
while i < rules.len() && matching_response.is_none() {
112112
let rule = &rules[i];
113113

114-
// Check if the rule is already exhausted
115-
if rule.is_exhausted() {
114+
// Check if the rule is already exhausted or if it's a simple rule used once
115+
//
116+
// In `aws-smithy-mocks-experimental` all rules were infinite sequences
117+
// but were only usable once in sequential mode. We retain that here for
118+
// backwards compatibility.
119+
if rule.is_exhausted() || (rule.is_simple() && rule.num_calls() > 0) {
116120
// Rule is exhausted, remove it and try the next one
117121
rules.remove(i);
118122
continue; // Don't increment i since we removed an element
@@ -444,7 +448,7 @@ mod tests {
444448
expected = "must_match was enabled but no rules matched or all rules were exhausted for"
445449
)]
446450
#[tokio::test]
447-
async fn test_exhausted_rules() {
451+
async fn test_exhausted_rules_sequential() {
448452
// Create a rule with a single response
449453
let rule = create_rule_builder().then_output(|| TestOutput::new("only response"));
450454

@@ -503,6 +507,14 @@ mod tests {
503507
// Verify the rules were used the expected number of times
504508
assert_eq!(rule1.num_calls(), 1);
505509
assert_eq!(rule2.num_calls(), 1);
510+
511+
// Calling with bucket1 again should match rule1 a second time
512+
let result1 = operation
513+
.invoke(TestInput::new("bucket1", "test-key"))
514+
.await;
515+
assert!(result1.is_ok());
516+
assert_eq!(result1.unwrap(), TestOutput::new("response1"));
517+
assert_eq!(rule1.num_calls(), 2);
506518
}
507519

508520
#[tokio::test]
@@ -553,9 +565,63 @@ mod tests {
553565
// Verify the rule was used the expected number of times
554566
assert_eq!(rule.num_calls(), 3);
555567
}
568+
#[tokio::test]
569+
async fn test_exhausted_sequence_match_any() {
570+
// Create a rule with a sequence that will be exhausted
571+
let rule = create_rule_builder()
572+
.match_requests(|input| input.bucket == "bucket-1")
573+
.sequence()
574+
.output(|| TestOutput::new("response 1"))
575+
.output(|| TestOutput::new("response 2"))
576+
.build();
577+
578+
// Create another rule to use after the first one is exhausted
579+
let fallback_rule =
580+
create_rule_builder().then_output(|| TestOutput::new("fallback response"));
581+
582+
// Create an interceptor with both rules
583+
let interceptor = MockResponseInterceptor::new()
584+
.rule_mode(RuleMode::MatchAny)
585+
.with_rule(&rule)
586+
.with_rule(&fallback_rule);
587+
588+
let operation = create_test_operation(interceptor, false);
589+
590+
// First two calls should use the first rule
591+
let result1 = operation
592+
.invoke(TestInput::new("bucket-1", "test-key"))
593+
.await;
594+
assert!(result1.is_ok());
595+
assert_eq!(result1.unwrap(), TestOutput::new("response 1"));
596+
597+
// second should use our fallback rule
598+
let result2 = operation
599+
.invoke(TestInput::new("other-bucket", "test-key"))
600+
.await;
601+
assert!(result2.is_ok());
602+
assert_eq!(result2.unwrap(), TestOutput::new("fallback response"));
603+
604+
// Third call should use the first rule again and exhaust it
605+
let result3 = operation
606+
.invoke(TestInput::new("bucket-1", "test-key"))
607+
.await;
608+
assert!(result3.is_ok());
609+
assert_eq!(result3.unwrap(), TestOutput::new("response 2"));
610+
611+
// first rule is exhausted so the matcher shouldn't matter and we should hit our fallback rule
612+
let result4 = operation
613+
.invoke(TestInput::new("bucket-1", "test-key"))
614+
.await;
615+
assert!(result4.is_ok());
616+
assert_eq!(result4.unwrap(), TestOutput::new("fallback response"));
617+
618+
// Verify the rules were used the expected number of times
619+
assert_eq!(rule.num_calls(), 2);
620+
assert_eq!(fallback_rule.num_calls(), 2);
621+
}
556622

557623
#[tokio::test]
558-
async fn test_exhausted_sequence() {
624+
async fn test_exhausted_sequence_sequential() {
559625
// Create a rule with a sequence that will be exhausted
560626
let rule = create_rule_builder()
561627
.sequence()
@@ -695,4 +761,118 @@ mod tests {
695761
assert_eq!(result2.unwrap(), TestOutput::new("success"));
696762
assert_eq!(rule2.num_calls(), 1);
697763
}
764+
765+
#[tokio::test]
766+
async fn test_simple_rule_in_match_any_mode() {
767+
let rule = create_rule_builder().then_output(|| TestOutput::new("simple response"));
768+
769+
let interceptor = MockResponseInterceptor::new()
770+
.rule_mode(RuleMode::MatchAny)
771+
.with_rule(&rule);
772+
773+
let operation = create_test_operation(interceptor, false);
774+
775+
for i in 0..5 {
776+
let result = operation
777+
.invoke(TestInput::new("test-bucket", "test-key"))
778+
.await;
779+
assert!(result.is_ok(), "Call {} should succeed", i);
780+
assert_eq!(result.unwrap(), TestOutput::new("simple response"));
781+
}
782+
assert_eq!(rule.num_calls(), 5);
783+
assert!(!rule.is_exhausted());
784+
}
785+
786+
#[tokio::test]
787+
async fn test_simple_rule_in_sequential_mode() {
788+
let rule1 = create_rule_builder().then_output(|| TestOutput::new("first response"));
789+
let rule2 = create_rule_builder().then_output(|| TestOutput::new("second response"));
790+
791+
let interceptor = MockResponseInterceptor::new()
792+
.rule_mode(RuleMode::Sequential)
793+
.with_rule(&rule1)
794+
.with_rule(&rule2);
795+
796+
let operation = create_test_operation(interceptor, false);
797+
798+
let result1 = operation
799+
.invoke(TestInput::new("test-bucket", "test-key"))
800+
.await;
801+
assert!(result1.is_ok());
802+
assert_eq!(result1.unwrap(), TestOutput::new("first response"));
803+
804+
// Second call should use rule2 (rule1 should be removed after one use in Sequential mode)
805+
let result2 = operation
806+
.invoke(TestInput::new("test-bucket", "test-key"))
807+
.await;
808+
assert!(result2.is_ok());
809+
assert_eq!(result2.unwrap(), TestOutput::new("second response"));
810+
811+
assert_eq!(rule1.num_calls(), 1);
812+
assert_eq!(rule2.num_calls(), 1);
813+
}
814+
815+
#[tokio::test]
816+
async fn test_repeatedly_method() {
817+
let rule = create_rule_builder()
818+
.sequence()
819+
.output(|| TestOutput::new("first response"))
820+
.output(|| TestOutput::new("repeated response"))
821+
.repeatedly()
822+
.build();
823+
824+
let interceptor = MockResponseInterceptor::new()
825+
.rule_mode(RuleMode::Sequential)
826+
.with_rule(&rule);
827+
828+
let operation = create_test_operation(interceptor, false);
829+
830+
let result1 = operation
831+
.invoke(TestInput::new("test-bucket", "test-key"))
832+
.await;
833+
assert!(result1.is_ok());
834+
assert_eq!(result1.unwrap(), TestOutput::new("first response"));
835+
836+
// all subsequent calls should return "repeated response"
837+
for i in 0..10 {
838+
let result = operation
839+
.invoke(TestInput::new("test-bucket", "test-key"))
840+
.await;
841+
assert!(result.is_ok(), "Call {} should succeed", i);
842+
assert_eq!(result.unwrap(), TestOutput::new("repeated response"));
843+
}
844+
assert_eq!(rule.num_calls(), 11);
845+
assert!(!rule.is_exhausted());
846+
}
847+
848+
#[should_panic(expected = "times(n) called before adding a response to the sequence")]
849+
#[test]
850+
fn test_times_validation() {
851+
// This should panic because times() is called before adding any responses
852+
let _rule = create_rule_builder()
853+
.sequence()
854+
.times(3)
855+
.output(|| TestOutput::new("response"))
856+
.build();
857+
}
858+
859+
#[should_panic(expected = "repeatedly() called before adding a response to the sequence")]
860+
#[test]
861+
fn test_repeatedly_validation() {
862+
// This should panic because repeatedly() is called before adding any responses
863+
let _rule = create_rule_builder().sequence().repeatedly().build();
864+
}
865+
866+
#[test]
867+
fn test_total_responses_overflow() {
868+
// Create a rule with a large number of repetitions to test overflow handling
869+
let rule = create_rule_builder()
870+
.sequence()
871+
.output(|| TestOutput::new("response"))
872+
.times(usize::MAX / 2)
873+
.output(|| TestOutput::new("another response"))
874+
.repeatedly()
875+
.build();
876+
assert_eq!(rule.max_responses, usize::MAX);
877+
}
698878
}

0 commit comments

Comments
 (0)