Skip to content

Commit 8cc1d1d

Browse files
authored
test: enhance duplicate validation with modular testcase logic (#4)
- Refactor verify_duplicate_testcase into organized duplicate_test module - Add comprehensive timestamp-based duplicate correctness validation - Improve test output formatting with clear PASSED/FILTERED status - Enhance error messages for better debugging of test failures - Add detailed debug output for duplicate detection timing validation The test infrastructure now provides precise validation of which duplicate was kept during deduplication, making it easier to catch Lambda bugs where wrong duplicates are retained instead of "last occurrence wins".
1 parent fbde6c3 commit 8cc1d1d

File tree

4 files changed

+201
-38
lines changed

4 files changed

+201
-38
lines changed

CHANGELOG.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ All notable changes to this project will be documented in this file.
55
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project uses [semantic versioning](https://semver.org/).
66

77
---
8-
98
## [Unreleased]
109

1110
### Changed
@@ -16,9 +15,23 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and
1615
- Added comprehensive boundary testing for time-based filtering rules
1716
- Self-documenting test cases with clear business rule descriptions
1817
- **Enhanced Test Reporting**: Added aligned, formatted test output for better readability
18+
- **Improved Test Validation**: Enhanced duplicate detection logic with precise timestamp validation
19+
- Added `verify_duplicate_testcase` function with comprehensive error messaging
20+
- Implemented detailed test case validation showing PASSED vs FILTERED status
21+
- Refactored test logic into organized `duplicate_test` module for better maintainability
22+
23+
### Added
24+
- **Comprehensive Edge Case Testing**: Dynamic test data generation for boundary conditions
25+
- Automated testing of 7-day and 90-day filtering boundaries
26+
- Real-time duplicate detection validation with timing precision
27+
- **Enhanced Test Output**: Clear, professional test result formatting
28+
- Color-coded test status (✅/❌) with descriptive failure messages
29+
- Detailed debug output for duplicate validation logic
1930

2031
### Technical
2132
- Improved test maintainability with descriptive entity names and test descriptions
33+
- Modularized duplicate testing logic for better code organization
34+
- Enhanced error messages for easier debugging of test failures
2235

2336
---
2437

Dockerfile.lambda-runtime

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ RUN rustup component add rustfmt
1818
# Install cargo-lambda
1919
RUN cargo install cargo-lambda --version 1.8.5 --locked --quiet
2020

21+
# Install clippy component
22+
RUN rustup component add clippy
23+
2124
# Default user
2225
RUN useradd -m dev
2326
WORKDIR /app

scripts/build.sh

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@ if ! nc -z localhost 9000; then
2222
exit 1
2323
fi
2424

25+
RUN="docker exec aws-lambda-action-filter-lambda-1"
2526

2627
# Run all tests inside the container
27-
docker exec aws-lambda-action-filter-lambda-1 cargo fmt --version
28-
docker exec aws-lambda-action-filter-lambda-1 cargo fmt --check
29-
docker exec aws-lambda-action-filter-lambda-1 cargo build --release --quiet
30-
docker exec aws-lambda-action-filter-lambda-1 cargo test --release --quiet
28+
${RUN} cargo fmt --version
29+
${RUN} cargo fmt --check
30+
${RUN} cargo clippy --quiet --all-features -- -D warnings
31+
${RUN} cargo build --release --quiet
32+
${RUN} cargo test --release --quiet -- --nocapture
3133

3234
echo "All tests passed!"

tests/edge_case_tests.rs

Lines changed: 178 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,18 @@ struct TestCase {
3737
description: &'static str,
3838
}
3939

40+
const DUP_TEST_ID: &str = "dedup_test_id";
41+
4042
#[rustfmt::skip]
4143
const EDGE_CASES: &[TestCase] = &[
42-
TestCase { entity_id: "dedup_first_occurrence", next_offset: 30, last_offset: -10, priority: Priority::Urgent, should_pass: true, description: "Tests deduplication (first occurrence)" },
43-
TestCase { entity_id: "dedup_first_occurrence", next_offset: 35, last_offset: -15, priority: Priority::Normal, should_pass: true, description: "Tests deduplication (last occurrence wins)" },
44-
TestCase { entity_id: "more_than_7_days_ago_fail", next_offset: 20, last_offset: -7, priority: Priority::Urgent, should_pass: false, description: "Tests 'more than 7 days ago' rule (should fail)" },
45-
TestCase { entity_id: "more_than_7_days_ago_pass", next_offset: 20, last_offset: -8, priority: Priority::Urgent, should_pass: true, description: "Tests 'more than 7 days ago' rule (should pass)" },
46-
TestCase { entity_id: "more_than_7_days_ago_pass_2", next_offset: 25, last_offset: -10, priority: Priority::Urgent, should_pass: true, description: "Tests 'more than 7 days ago' rule (should pass)" },
47-
TestCase { entity_id: "within_90_days_fail", next_offset: 91, last_offset: -30, priority: Priority::Normal, should_pass: false, description: "Tests 'within 90 days' rule (should fail at 91 days)" },
48-
TestCase { entity_id: "within_90_days_pass", next_offset: 90, last_offset: -30, priority: Priority::Normal, should_pass: true, description: "Tests 'within 90 days' rule boundary (should pass)" },
49-
TestCase { entity_id: "within_90_days_pass_2", next_offset: 89, last_offset: -20, priority: Priority::Normal, should_pass: true, description: "Tests 'within 90 days' rule (should pass)" },
44+
TestCase { entity_id: DUP_TEST_ID, next_offset: 30, last_offset: -10, priority: Priority::Urgent, should_pass: false, description: "Rule: deduplication (first occurrence, expected to be FILTERED)" },
45+
TestCase { entity_id: DUP_TEST_ID, next_offset: 35, last_offset: -15, priority: Priority::Normal, should_pass: true, description: "Rule: deduplication (last occurrence wins, expected to be PASSED)" },
46+
TestCase { entity_id: "more_than_7_days_ago_fail", next_offset: 20, last_offset: -7, priority: Priority::Urgent, should_pass: false, description: "Rule: more than 7 days ago (fail <7)" },
47+
TestCase { entity_id: "more_than_7_days_ago_pass", next_offset: 20, last_offset: -8, priority: Priority::Urgent, should_pass: true, description: "Rule: more than 7 days ago (pass =7)" },
48+
TestCase { entity_id: "more_than_7_days_ago_pass_2", next_offset: 25, last_offset: -10, priority: Priority::Urgent, should_pass: true, description: "Rule: more than 7 days ago (pass >7)" },
49+
TestCase { entity_id: "within_90_days_fail", next_offset: 91, last_offset: -30, priority: Priority::Normal, should_pass: false, description: "Rule: within 90 days (fail >90)" },
50+
TestCase { entity_id: "within_90_days_pass", next_offset: 90, last_offset: -30, priority: Priority::Normal, should_pass: true, description: "Rule: within 90 days (pass =90)" },
51+
TestCase { entity_id: "within_90_days_pass_2", next_offset: 89, last_offset: -20, priority: Priority::Normal, should_pass: true, description: "Rule: within 90 days (pass <90)" },
5052
];
5153

5254
fn create_action(
@@ -84,51 +86,68 @@ fn generate_test_data() -> Result<String> {
8486
Ok(json)
8587
}
8688

87-
fn verify_test_expectations(results: &[Action]) -> Result<()> {
89+
fn verify_test_expectations(results: &[Action]) -> (bool, Vec<String>) {
8890
// ---
89-
let prefix = "verify_test_expectations";
90-
9191
// Convert results to a map for O(1) lookup
9292
let result_map: HashMap<&str, &Action> =
9393
results.iter().map(|action| (action.entity_id.as_str(), action)).collect();
9494

95+
let mut lines = Vec::new();
96+
let mut passed = true; // To be set only if setting to false
97+
9598
// Iterate over test expectations and verify against results
9699
for test_case in EDGE_CASES {
97100
// ---
98-
let found_in_results = result_map.contains_key(test_case.entity_id);
101+
let found = result_map.contains_key(test_case.entity_id);
102+
103+
if test_case.entity_id == DUP_TEST_ID {
104+
// ---
105+
let (pass, line) =
106+
duplicate_test::verify_duplicate_testcase(test_case, &result_map, found);
107+
108+
lines.push(line);
109+
110+
if !pass {
111+
passed = false;
112+
}
113+
continue; // Skip to next test case
114+
}
99115

100-
match (test_case.should_pass, found_in_results) {
116+
match (test_case.should_pass, found) {
101117
// ---
102118
(true, false) => {
103119
// ---
104-
ensure!(
105-
false,
106-
"{prefix}: {} - Expected to pass but was filtered out. {}",
107-
test_case.entity_id,
108-
test_case.description
109-
);
120+
lines.push(format!(
121+
"❌ {:<28}: FILTERED - Expected to PASS but was not found in results. {}-2",
122+
test_case.entity_id, test_case.description
123+
));
124+
passed = false;
110125
}
111126
(false, true) => {
112127
// ---
113-
ensure!(
114-
false,
115-
"{prefix}: {} - Expected to be filtered out but found in results. {}",
116-
test_case.entity_id,
117-
test_case.description
118-
);
128+
lines.push(format!(
129+
"❌ {:<28}: FILTERED - Expected to be FILTERED but was found in results. {}-3",
130+
test_case.entity_id, test_case.description
131+
));
132+
passed = false;
119133
}
120134
(true, true) => {
121135
// ---
122-
println!("✓ {:<28}: PASS - {}", test_case.entity_id, test_case.description);
136+
lines.push(format!(
137+
"✅ {:<28}: PASSED - {}",
138+
test_case.entity_id, test_case.description
139+
));
123140
}
124141
(false, false) => {
125142
// ---
126-
println!("✓ {:<28}: FILTERED - {}", test_case.entity_id, test_case.description);
143+
lines.push(format!(
144+
"✅ {:<28}: FILTERED - {}",
145+
test_case.entity_id, test_case.description
146+
));
127147
}
128148
}
129149
}
130-
131-
Ok(())
150+
(passed, lines)
132151
}
133152

134153
#[test]
@@ -152,13 +171,22 @@ fn test_dynamic_edge_cases() -> Result<()> {
152171
let results = run_lambda_invoke(temp_file)?;
153172

154173
println!("Lambda returned {} actions", results.len());
174+
results.iter().for_each(|action| {
175+
println!(" :: {}", action.entity_id);
176+
});
177+
print!("---\n\n");
155178

156179
// Verify all test expectations
157-
verify_test_expectations(&results)?;
180+
let (passed, lines) = verify_test_expectations(&results);
181+
let lines = lines.join("\n");
182+
183+
println!("\nAll TestCase Results:");
184+
println!("{lines}");
185+
ensure!(passed, "Test failed");
158186

159187
// Additional verification: check expected count
160188
// Should have 5 actions: 6 that should pass - 1 duplicate = 5
161-
// (dedup_first_occurrence appears twice but deduplicated to 1)
189+
// (DUP_TEST_ID appears twice but deduplicated to 1)
162190
let expected_count = 5;
163191
ensure!(
164192
results.len() == expected_count,
@@ -181,8 +209,7 @@ fn test_dynamic_edge_cases() -> Result<()> {
181209
}
182210

183211
// Verify deduplication worked correctly
184-
let duplicate_count =
185-
results.iter().filter(|a| a.entity_id == "dedup_first_occurrence").count();
212+
let duplicate_count = results.iter().filter(|a| a.entity_id == DUP_TEST_ID).count();
186213
ensure!(
187214
duplicate_count == 1,
188215
"Expected exactly 1 'duplicate' entity after deduplication, found {}",
@@ -210,3 +237,121 @@ fn test_dynamic_edge_cases() -> Result<()> {
210237

211238
Ok(())
212239
}
240+
241+
mod duplicate_test {
242+
// ---
243+
use super::{Action, TestCase};
244+
use chrono::{Duration, Utc};
245+
use std::collections::HashMap;
246+
247+
pub fn verify_duplicate_testcase(
248+
test_case: &TestCase,
249+
result_map: &HashMap<&str, &Action>,
250+
is_found: bool,
251+
) -> (bool, String) {
252+
// ---
253+
// Early return: Handle case where entity was not found in results
254+
if !is_found {
255+
// ---
256+
return handle_entity_not_found(test_case);
257+
}
258+
259+
// Entity was found - check if it's the correct duplicate
260+
let duplicate_check_result = check_duplicate_correctness(test_case, result_map);
261+
262+
match (test_case.should_pass, duplicate_check_result.is_correct) {
263+
// ---
264+
(true, true) => {
265+
// Expected to pass, and correct duplicate was kept
266+
(true, format_success_message(test_case, "PASSED"))
267+
}
268+
(true, false) => {
269+
// Expected to pass, but wrong duplicate was kept
270+
(false, format_wrong_duplicate_error(test_case, &duplicate_check_result))
271+
}
272+
(false, true) => {
273+
// Expected to be filtered, but correct duplicate was kept (this shouldn't happen)
274+
(false, format_unexpected_correct_duplicate_error(test_case))
275+
}
276+
(false, false) => {
277+
// Expected to be filtered, and wrong duplicate was kept (which is fine)
278+
(true, format_success_message(test_case, "FILTERED"))
279+
}
280+
}
281+
}
282+
283+
// Helper function to handle when entity is not found in results
284+
fn handle_entity_not_found(test_case: &TestCase) -> (bool, String) {
285+
// ---
286+
if test_case.should_pass {
287+
// Expected to pass but was filtered out
288+
(
289+
false,
290+
format!(
291+
"❌ {:<28}: FAILED - {} Expected to PASS but was FILTERED.",
292+
test_case.entity_id, test_case.description
293+
),
294+
)
295+
} else {
296+
// Expected to be filtered and was filtered
297+
(
298+
true,
299+
format!(
300+
"✅ {:<28}: PASSED - {} (was not passed, which is what we expected)",
301+
test_case.entity_id, test_case.description
302+
),
303+
)
304+
}
305+
}
306+
307+
// Struct to hold duplicate correctness check results
308+
struct DuplicateCheckResult {
309+
is_correct: bool,
310+
time_diff: i64,
311+
}
312+
313+
// Helper function to check if the correct duplicate was kept
314+
fn check_duplicate_correctness(
315+
test_case: &TestCase,
316+
result_map: &HashMap<&str, &Action>,
317+
) -> DuplicateCheckResult {
318+
// ---
319+
let action = result_map.get(test_case.entity_id).unwrap();
320+
let now = Utc::now();
321+
let expected_next_time = now + Duration::days(test_case.next_offset);
322+
323+
let time_diff = (action.next_action_time - expected_next_time).num_seconds().abs();
324+
let is_correct = time_diff < 60; // Allow small time differences
325+
326+
DuplicateCheckResult { is_correct, time_diff }
327+
}
328+
329+
// Helper functions for formatting messages
330+
fn format_success_message(test_case: &TestCase, status: &str) -> String {
331+
// ---
332+
format!("✅ {:<28}: {:<8} - {}", test_case.entity_id, status, test_case.description)
333+
}
334+
335+
fn format_wrong_duplicate_error(
336+
test_case: &TestCase,
337+
check_result: &DuplicateCheckResult,
338+
) -> String {
339+
// ---
340+
format!(
341+
"❌ {:<28}: FILTERED - {}. Wrong DUPLICATE kept, \
342+
Expected next_offset: {}, but found action with different timing. {}",
343+
test_case.entity_id,
344+
test_case.description,
345+
test_case.next_offset,
346+
check_result.time_diff
347+
)
348+
}
349+
350+
fn format_unexpected_correct_duplicate_error(test_case: &TestCase) -> String {
351+
// ---
352+
format!(
353+
"❌ {:<28}: FAILED - {} Expected to be FILTERED but correct duplicate was found in results.",
354+
test_case.entity_id, test_case.description
355+
)
356+
}
357+
}

0 commit comments

Comments
 (0)