re-uploading work
This commit is contained in:
325
tests/EXECUTION_FILTERING.md
Normal file
325
tests/EXECUTION_FILTERING.md
Normal file
@@ -0,0 +1,325 @@
|
||||
# Execution Filtering Best Practices for E2E Tests
|
||||
|
||||
## Problem Overview
|
||||
|
||||
When writing E2E tests that verify execution creation, you may encounter race conditions or filtering issues where the test cannot find the executions it just created. This happens because:
|
||||
|
||||
1. **Imprecise filtering** - Using `action_ref` alone can match executions from other tests
|
||||
2. **Data pollution** - Old executions from previous test runs aren't cleaned up
|
||||
3. **Timing issues** - Executions haven't been created yet when the query runs
|
||||
4. **Parallel execution** - Multiple tests creating similar resources simultaneously
|
||||
|
||||
## Solution: Multi-Level Filtering
|
||||
|
||||
The `wait_for_execution_count` helper now supports multiple filtering strategies that can be combined for maximum precision:
|
||||
|
||||
### 1. Rule-Based Filtering (Most Precise)
|
||||
|
||||
Filter executions by the rule that triggered them:
|
||||
|
||||
```python
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# Capture timestamp before creating rule
|
||||
rule_creation_time = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
# Create your automation
|
||||
rule = create_rule(client, trigger_id=trigger['id'], action_ref=action['ref'])
|
||||
|
||||
# Wait for executions using rule_id
|
||||
executions = wait_for_execution_count(
|
||||
client=client,
|
||||
expected_count=3,
|
||||
rule_id=rule['id'], # Filter by rule
|
||||
created_after=rule_creation_time, # Only new executions
|
||||
timeout=30,
|
||||
verbose=True # Enable debug output
|
||||
)
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
1. Gets all enforcements for the rule: `GET /api/v1/enforcements?rule_id=<id>`
|
||||
2. For each enforcement, gets executions: `GET /api/v1/executions?enforcement=<id>`
|
||||
3. Filters by timestamp to exclude old data
|
||||
4. Returns combined results
|
||||
|
||||
### 2. Enforcement-Based Filtering (Very Precise)
|
||||
|
||||
If you have a specific enforcement ID:
|
||||
|
||||
```python
|
||||
executions = wait_for_execution_count(
|
||||
client=client,
|
||||
expected_count=1,
|
||||
enforcement_id=enforcement['id'],
|
||||
timeout=30
|
||||
)
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
- Directly queries: `GET /api/v1/executions?enforcement=<id>`
|
||||
- Most direct and precise filtering
|
||||
|
||||
### 3. Action-Based Filtering (Less Precise)
|
||||
|
||||
When you only have an action reference:
|
||||
|
||||
```python
|
||||
from datetime import datetime, timezone
|
||||
|
||||
action_creation_time = datetime.now(timezone.utc).isoformat()
|
||||
action = create_echo_action(client, pack_ref=pack_ref)
|
||||
|
||||
executions = wait_for_execution_count(
|
||||
client=client,
|
||||
expected_count=5,
|
||||
action_ref=action['ref'],
|
||||
created_after=action_creation_time, # Important!
|
||||
timeout=30
|
||||
)
|
||||
```
|
||||
|
||||
**Important:** Always use `created_after` with action_ref filtering to avoid matching old executions.
|
||||
|
||||
### 4. Status Filtering
|
||||
|
||||
Combine with any of the above:
|
||||
|
||||
```python
|
||||
executions = wait_for_execution_count(
|
||||
client=client,
|
||||
expected_count=3,
|
||||
rule_id=rule['id'],
|
||||
status='succeeded', # Only succeeded executions
|
||||
timeout=30
|
||||
)
|
||||
```
|
||||
|
||||
## Timestamp-Based Filtering
|
||||
|
||||
The `created_after` parameter filters executions created after a specific ISO timestamp:
|
||||
|
||||
```python
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# Capture timestamp at start of test
|
||||
test_start = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
# ... create automation ...
|
||||
|
||||
# Only count executions created during this test
|
||||
executions = wait_for_execution_count(
|
||||
client=client,
|
||||
expected_count=3,
|
||||
created_after=test_start,
|
||||
# ... other filters ...
|
||||
)
|
||||
```
|
||||
|
||||
This prevents:
|
||||
- Matching executions from previous test runs
|
||||
- Counting executions from test setup/fixtures
|
||||
- Race conditions with parallel tests
|
||||
|
||||
## Verbose Mode for Debugging
|
||||
|
||||
Enable verbose mode to see what's being matched:
|
||||
|
||||
```python
|
||||
executions = wait_for_execution_count(
|
||||
client=client,
|
||||
expected_count=3,
|
||||
rule_id=rule['id'],
|
||||
verbose=True # Print debug output
|
||||
)
|
||||
```
|
||||
|
||||
Output example:
|
||||
```
|
||||
[DEBUG] Found 2 enforcements for rule 123
|
||||
[DEBUG] Enforcement 456: 3 executions
|
||||
[DEBUG] Enforcement 457: 2 executions
|
||||
[DEBUG] After timestamp filter: 3 executions (was 5)
|
||||
[DEBUG] Checking: 3 >= 3
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ DO: Use Multiple Filter Criteria
|
||||
|
||||
```python
|
||||
# GOOD - Multiple precise filters
|
||||
executions = wait_for_execution_count(
|
||||
client=client,
|
||||
expected_count=3,
|
||||
rule_id=rule['id'], # Precise filter
|
||||
created_after=rule_created, # Timestamp filter
|
||||
status='succeeded', # State filter
|
||||
verbose=True # Debugging
|
||||
)
|
||||
```
|
||||
|
||||
### ❌ DON'T: Use Only action_ref
|
||||
|
||||
```python
|
||||
# BAD - Too imprecise, may match old data
|
||||
executions = wait_for_execution_count(
|
||||
client=client,
|
||||
expected_count=3,
|
||||
action_ref=action['ref'] # Could match previous runs
|
||||
)
|
||||
```
|
||||
|
||||
### ✅ DO: Capture Timestamps Early
|
||||
|
||||
```python
|
||||
# GOOD - Timestamp before resource creation
|
||||
test_start = datetime.now(timezone.utc).isoformat()
|
||||
rule = create_rule(...)
|
||||
executions = wait_for_execution_count(..., created_after=test_start)
|
||||
```
|
||||
|
||||
### ❌ DON'T: Capture Timestamps After Waiting
|
||||
|
||||
```python
|
||||
# BAD - Timestamp is too late
|
||||
rule = create_rule(...)
|
||||
time.sleep(10) # Events already created
|
||||
test_start = datetime.now(timezone.utc).isoformat()
|
||||
executions = wait_for_execution_count(..., created_after=test_start) # Will miss executions!
|
||||
```
|
||||
|
||||
### ✅ DO: Use rule_id When Testing Automation Flows
|
||||
|
||||
```python
|
||||
# GOOD - For trigger → rule → execution flows
|
||||
executions = wait_for_execution_count(
|
||||
client=client,
|
||||
expected_count=3,
|
||||
rule_id=rule['id'] # Most natural for automation tests
|
||||
)
|
||||
```
|
||||
|
||||
### ✅ DO: Use enforcement_id When Testing Specific Enforcements
|
||||
|
||||
```python
|
||||
# GOOD - For testing single enforcement
|
||||
enforcement = enforcements[0]
|
||||
executions = wait_for_execution_count(
|
||||
client=client,
|
||||
expected_count=1,
|
||||
enforcement_id=enforcement['id']
|
||||
)
|
||||
```
|
||||
|
||||
## Filter Hierarchy (Precision Order)
|
||||
|
||||
From most precise to least precise:
|
||||
|
||||
1. **enforcement_id** - Single enforcement's executions
|
||||
2. **rule_id** - All executions from a rule (via enforcements)
|
||||
3. **action_ref** + **created_after** - Executions of an action created recently
|
||||
4. **action_ref** alone - All executions of an action (can match old data)
|
||||
|
||||
## API Endpoints Used
|
||||
|
||||
The helper uses these API endpoints internally:
|
||||
|
||||
```
|
||||
GET /api/v1/executions?enforcement=<id> # enforcement_id filter
|
||||
GET /api/v1/enforcements?rule_id=<id> # rule_id filter (step 1)
|
||||
GET /api/v1/executions?enforcement=<id> # rule_id filter (step 2)
|
||||
GET /api/v1/executions?action_ref=<ref> # action_ref filter
|
||||
GET /api/v1/executions?status=<status> # status filter
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```python
|
||||
from datetime import datetime, timezone
|
||||
from helpers import (
|
||||
AttuneClient,
|
||||
create_interval_timer,
|
||||
create_echo_action,
|
||||
create_rule,
|
||||
wait_for_event_count,
|
||||
wait_for_execution_count,
|
||||
)
|
||||
|
||||
def test_timer_automation(client: AttuneClient, pack_ref: str):
|
||||
"""Complete example with proper filtering"""
|
||||
|
||||
# Capture timestamp at start
|
||||
test_start = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
# Create automation components
|
||||
trigger = create_interval_timer(client, interval_seconds=5, pack_ref=pack_ref)
|
||||
action = create_echo_action(client, pack_ref=pack_ref)
|
||||
rule = create_rule(
|
||||
client,
|
||||
trigger_id=trigger['id'],
|
||||
action_ref=action['ref'],
|
||||
pack_ref=pack_ref
|
||||
)
|
||||
|
||||
# Wait for events
|
||||
events = wait_for_event_count(
|
||||
client=client,
|
||||
expected_count=3,
|
||||
trigger_id=trigger['id'],
|
||||
timeout=20
|
||||
)
|
||||
|
||||
# Wait for executions with precise filtering
|
||||
executions = wait_for_execution_count(
|
||||
client=client,
|
||||
expected_count=3,
|
||||
rule_id=rule['id'], # Precise: only this rule's executions
|
||||
created_after=test_start, # Only executions from this test
|
||||
status='succeeded', # Only successful ones
|
||||
timeout=30,
|
||||
verbose=True # Debug output
|
||||
)
|
||||
|
||||
# Verify results
|
||||
assert len(executions) == 3
|
||||
for exec in executions:
|
||||
assert exec['status'] == 'succeeded'
|
||||
assert exec['action_ref'] == action['ref']
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Test finds too many executions
|
||||
|
||||
**Cause:** Not filtering by timestamp, matching old data
|
||||
**Solution:** Add `created_after` parameter
|
||||
|
||||
### Test finds too few executions
|
||||
|
||||
**Cause:** Timestamp captured too late, after executions created
|
||||
**Solution:** Capture timestamp BEFORE creating rule/trigger
|
||||
|
||||
### Test times out waiting for executions
|
||||
|
||||
**Cause:** Executions not being created (service issue)
|
||||
**Solution:** Enable `verbose=True` to see what's being found, check service logs
|
||||
|
||||
### Inconsistent test results
|
||||
|
||||
**Cause:** Race condition with database cleanup or parallel tests
|
||||
**Solution:** Use `rule_id` filtering for isolation
|
||||
|
||||
## Summary
|
||||
|
||||
**Always prefer:**
|
||||
1. `rule_id` for automation flow tests (trigger → rule → execution)
|
||||
2. `enforcement_id` for specific enforcement tests
|
||||
3. `created_after` to prevent matching old data
|
||||
4. `verbose=True` when debugging
|
||||
|
||||
**This ensures:**
|
||||
- ✅ Test isolation
|
||||
- ✅ No race conditions
|
||||
- ✅ Precise execution matching
|
||||
- ✅ Easy debugging
|
||||
Reference in New Issue
Block a user