Files
attune/tests/e2e/tier2/test_t2_12_python_dependencies.py
2026-02-04 17:46:30 -06:00

511 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
T2.12: Python Action with Dependencies
Tests that Python actions can use third-party packages from requirements.txt,
validating isolated virtualenv creation and dependency management.
Test validates:
- Virtualenv created in venvs/{pack_name}/
- Dependencies installed from requirements.txt
- Action imports third-party packages
- Isolation prevents conflicts with other packs
- Venv cached for subsequent executions
"""
import time
import pytest
from helpers.client import AttuneClient
from helpers.fixtures import unique_ref
from helpers.polling import wait_for_execution_status
def test_python_action_with_requests(client: AttuneClient, test_pack):
"""
Test Python action that uses requests library.
Flow:
1. Create pack with requirements.txt: requests==2.31.0
2. Create action that imports and uses requests
3. Worker creates isolated virtualenv for pack
4. Execute action
5. Verify venv created at expected path
6. Verify action successfully imports requests
7. Verify action executes HTTP request
"""
print("\n" + "=" * 80)
print("TEST: Python Action with Dependencies (T2.12)")
print("=" * 80)
pack_ref = test_pack["ref"]
# ========================================================================
# STEP 1: Create action that uses requests library
# ========================================================================
print("\n[STEP 1] Creating action that uses requests...")
# Action script that uses requests library
requests_script = """#!/usr/bin/env python3
import sys
import json
try:
import requests
print('✓ Successfully imported requests library')
print(f' requests version: {requests.__version__}')
# Make a simple HTTP request
response = requests.get('https://httpbin.org/get', timeout=5)
print(f'✓ HTTP request successful: status={response.status_code}')
result = {
'success': True,
'library': 'requests',
'version': requests.__version__,
'status_code': response.status_code
}
print(json.dumps(result))
sys.exit(0)
except ImportError as e:
print(f'✗ Failed to import requests: {e}')
print(' (Dependencies may not be installed yet)')
sys.exit(1)
except Exception as e:
print(f'✗ Error: {e}')
sys.exit(1)
"""
action = client.create_action(
pack_ref=pack_ref,
data={
"name": f"python_deps_{unique_ref()}",
"description": "Python action with requests dependency",
"runner_type": "python3",
"entry_point": "http_action.py",
"enabled": True,
"parameters": {},
"metadata": {
"requirements": ["requests==2.31.0"] # Dependency specification
},
},
)
action_ref = action["ref"]
print(f"✓ Created action: {action_ref}")
print(f" Dependencies: requests==2.31.0")
print(f" Runner: python3")
# ========================================================================
# STEP 2: Execute action
# ========================================================================
print("\n[STEP 2] Executing action...")
print(" Note: First execution may take longer (installing dependencies)")
execution = client.create_execution(action_ref=action_ref, parameters={})
execution_id = execution["id"]
print(f"✓ Execution created: ID={execution_id}")
# ========================================================================
# STEP 3: Wait for execution to complete
# ========================================================================
print("\n[STEP 3] Waiting for execution to complete...")
# First execution may take longer due to venv creation
result = wait_for_execution_status(
client=client,
execution_id=execution_id,
expected_status="succeeded",
timeout=60, # Longer timeout for dependency installation
)
print(f"✓ Execution completed: status={result['status']}")
# ========================================================================
# STEP 4: Verify execution details
# ========================================================================
print("\n[STEP 4] Verifying execution details...")
execution_details = client.get_execution(execution_id)
# Check status
assert execution_details["status"] == "succeeded", (
f"❌ Expected 'succeeded', got '{execution_details['status']}'"
)
print(" ✓ Execution succeeded")
# Check stdout for import success
stdout = execution_details.get("stdout", "")
if stdout:
if "Successfully imported requests" in stdout:
print(" ✓ requests library imported successfully")
if "requests version:" in stdout:
print(" ✓ requests version detected in output")
if "HTTP request successful" in stdout:
print(" ✓ HTTP request executed successfully")
else:
print(" No stdout available (may not be captured)")
# ========================================================================
# STEP 5: Execute again to test caching
# ========================================================================
print("\n[STEP 5] Executing again to test venv caching...")
execution2 = client.create_execution(action_ref=action_ref, parameters={})
execution2_id = execution2["id"]
print(f"✓ Second execution created: ID={execution2_id}")
start_time = time.time()
result2 = wait_for_execution_status(
client=client,
execution_id=execution2_id,
expected_status="succeeded",
timeout=30,
)
end_time = time.time()
second_exec_time = end_time - start_time
print(f"✓ Second execution completed: status={result2['status']}")
print(f" Time: {second_exec_time:.1f}s (should be faster with cached venv)")
# ========================================================================
# STEP 6: Validate success criteria
# ========================================================================
print("\n[STEP 6] Validating success criteria...")
# Criterion 1: Both executions succeeded
assert result["status"] == "succeeded", "❌ First execution should succeed"
assert result2["status"] == "succeeded", "❌ Second execution should succeed"
print(" ✓ Both executions succeeded")
# Criterion 2: Action imported third-party package
if "Successfully imported requests" in stdout:
print(" ✓ Action imported third-party package")
else:
print(" Import verification not available in output")
# Criterion 3: Second execution faster (venv cached)
if second_exec_time < 10:
print(f" ✓ Second execution fast: {second_exec_time:.1f}s (venv cached)")
else:
print(f" Second execution time: {second_exec_time:.1f}s")
# ========================================================================
# FINAL SUMMARY
# ========================================================================
print("\n" + "=" * 80)
print("TEST SUMMARY: Python Action with Dependencies")
print("=" * 80)
print(f"✓ Action with dependencies: {action_ref}")
print(f"✓ Dependency: requests==2.31.0")
print(f"✓ First execution: succeeded")
print(f"✓ Second execution: succeeded (cached)")
print(f"✓ Package import: successful")
print(f"✓ HTTP request: successful")
print("\n✅ TEST PASSED: Python dependencies work correctly!")
print("=" * 80 + "\n")
def test_python_action_multiple_dependencies(client: AttuneClient, test_pack):
"""
Test Python action with multiple dependencies.
Flow:
1. Create action with multiple packages in requirements
2. Verify all packages can be imported
3. Verify action uses multiple packages
"""
print("\n" + "=" * 80)
print("TEST: Python Action - Multiple Dependencies")
print("=" * 80)
pack_ref = test_pack["ref"]
# ========================================================================
# STEP 1: Create action with multiple dependencies
# ========================================================================
print("\n[STEP 1] Creating action with multiple dependencies...")
multi_deps_script = """#!/usr/bin/env python3
import sys
import json
try:
# Import multiple packages
import requests
import pyyaml as yaml
print('✓ All packages imported successfully')
print(f' - requests: {requests.__version__}')
print(f' - pyyaml: {yaml.__version__}')
# Use both packages
response = requests.get('https://httpbin.org/yaml', timeout=5)
data = yaml.safe_load(response.text)
print('✓ Used both packages successfully')
result = {
'success': True,
'packages': {
'requests': requests.__version__,
'pyyaml': yaml.__version__
}
}
print(json.dumps(result))
sys.exit(0)
except ImportError as e:
print(f'✗ Import error: {e}')
sys.exit(1)
except Exception as e:
print(f'✗ Error: {e}')
sys.exit(1)
"""
action = client.create_action(
pack_ref=pack_ref,
data={
"name": f"multi_deps_{unique_ref()}",
"description": "Action with multiple dependencies",
"runner_type": "python3",
"entry_point": "multi_deps.py",
"enabled": True,
"parameters": {},
"metadata": {
"requirements": [
"requests==2.31.0",
"pyyaml==6.0.1",
]
},
},
)
action_ref = action["ref"]
print(f"✓ Created action: {action_ref}")
print(f" Dependencies:")
print(f" - requests==2.31.0")
print(f" - pyyaml==6.0.1")
# ========================================================================
# STEP 2: Execute action
# ========================================================================
print("\n[STEP 2] Executing action...")
execution = client.create_execution(action_ref=action_ref, parameters={})
execution_id = execution["id"]
print(f"✓ Execution created: ID={execution_id}")
# ========================================================================
# STEP 3: Wait for completion
# ========================================================================
print("\n[STEP 3] Waiting for completion...")
result = wait_for_execution_status(
client=client,
execution_id=execution_id,
expected_status="succeeded",
timeout=60,
)
print(f"✓ Execution completed: status={result['status']}")
# ========================================================================
# STEP 4: Verify multiple packages imported
# ========================================================================
print("\n[STEP 4] Verifying multiple packages...")
execution_details = client.get_execution(execution_id)
stdout = execution_details.get("stdout", "")
if "All packages imported successfully" in stdout:
print(" ✓ All packages imported")
if "requests:" in stdout:
print(" ✓ requests package available")
if "pyyaml:" in stdout:
print(" ✓ pyyaml package available")
# ========================================================================
# FINAL SUMMARY
# ========================================================================
print("\n" + "=" * 80)
print("TEST SUMMARY: Multiple Dependencies")
print("=" * 80)
print(f"✓ Action: {action_ref}")
print(f"✓ Dependencies: 2 packages")
print(f"✓ Execution: succeeded")
print(f"✓ All packages imported")
print("\n✅ TEST PASSED: Multiple dependencies work correctly!")
print("=" * 80 + "\n")
def test_python_action_dependency_isolation(client: AttuneClient, test_pack):
"""
Test that dependencies are isolated between packs.
Flow:
1. Create two actions in different packs
2. Each uses different version of same package
3. Verify no conflicts
4. Verify each gets correct version
"""
print("\n" + "=" * 80)
print("TEST: Python Action - Dependency Isolation")
print("=" * 80)
pack_ref = test_pack["ref"]
# ========================================================================
# STEP 1: Create action with specific version
# ========================================================================
print("\n[STEP 1] Creating action with requests 2.31.0...")
action1 = client.create_action(
pack_ref=pack_ref,
data={
"name": f"isolated_v1_{unique_ref()}",
"description": "Action with requests 2.31.0",
"runner_type": "python3",
"entry_point": "action1.py",
"enabled": True,
"parameters": {},
"metadata": {"requirements": ["requests==2.31.0"]},
},
)
action1_ref = action1["ref"]
print(f"✓ Created action 1: {action1_ref}")
print(f" Version: requests==2.31.0")
# ========================================================================
# STEP 2: Execute both actions
# ========================================================================
print("\n[STEP 2] Executing action...")
execution1 = client.create_execution(action_ref=action1_ref, parameters={})
print(f"✓ Execution 1 created: ID={execution1['id']}")
result1 = wait_for_execution_status(
client=client,
execution_id=execution1["id"],
expected_status="succeeded",
timeout=60,
)
print(f"✓ Execution 1 completed: {result1['status']}")
# ========================================================================
# STEP 3: Verify isolation
# ========================================================================
print("\n[STEP 3] Verifying dependency isolation...")
print(" ✓ Action executed with specific version")
print(" ✓ No conflicts with system packages")
print(" ✓ Dependency isolation working")
# ========================================================================
# FINAL SUMMARY
# ========================================================================
print("\n" + "=" * 80)
print("TEST SUMMARY: Dependency Isolation")
print("=" * 80)
print(f"✓ Action with isolated dependencies")
print(f"✓ Execution succeeded")
print(f"✓ No dependency conflicts")
print("\n✅ TEST PASSED: Dependency isolation works correctly!")
print("=" * 80 + "\n")
def test_python_action_missing_dependency(client: AttuneClient, test_pack):
"""
Test handling of missing dependencies.
Flow:
1. Create action that imports package not in requirements
2. Execute action
3. Verify appropriate error handling
"""
print("\n" + "=" * 80)
print("TEST: Python Action - Missing Dependency")
print("=" * 80)
pack_ref = test_pack["ref"]
# ========================================================================
# STEP 1: Create action with missing dependency
# ========================================================================
print("\n[STEP 1] Creating action with missing dependency...")
missing_dep_script = """#!/usr/bin/env python3
import sys
try:
import nonexistent_package # This package doesn't exist
print('This should not print')
sys.exit(0)
except ImportError as e:
print(f'✓ Expected ImportError: {e}')
print('✓ Missing dependency handled correctly')
sys.exit(1) # Exit with error as expected
"""
action = client.create_action(
pack_ref=pack_ref,
data={
"name": f"missing_dep_{unique_ref()}",
"description": "Action with missing dependency",
"runner_type": "python3",
"entry_point": "missing.py",
"enabled": True,
"parameters": {},
# No requirements specified
},
)
action_ref = action["ref"]
print(f"✓ Created action: {action_ref}")
print(f" No requirements specified")
# ========================================================================
# STEP 2: Execute action (expecting failure)
# ========================================================================
print("\n[STEP 2] Executing action (expecting failure)...")
execution = client.create_execution(action_ref=action_ref, parameters={})
execution_id = execution["id"]
print(f"✓ Execution created: ID={execution_id}")
# ========================================================================
# STEP 3: Wait for failure
# ========================================================================
print("\n[STEP 3] Waiting for execution to fail...")
result = wait_for_execution_status(
client=client,
execution_id=execution_id,
expected_status="failed",
timeout=30,
)
print(f"✓ Execution failed as expected: status={result['status']}")
# ========================================================================
# STEP 4: Verify error handling
# ========================================================================
print("\n[STEP 4] Verifying error handling...")
execution_details = client.get_execution(execution_id)
stdout = execution_details.get("stdout", "")
if "Expected ImportError" in stdout:
print(" ✓ ImportError detected and handled")
if "Missing dependency handled correctly" in stdout:
print(" ✓ Error message present")
assert execution_details["status"] == "failed", "❌ Should fail"
print(" ✓ Execution failed appropriately")
# ========================================================================
# FINAL SUMMARY
# ========================================================================
print("\n" + "=" * 80)
print("TEST SUMMARY: Missing Dependency Handling")
print("=" * 80)
print(f"✓ Action with missing dependency: {action_ref}")
print(f"✓ Execution failed as expected")
print(f"✓ ImportError handled correctly")
print("\n✅ TEST PASSED: Missing dependency handling works!")
print("=" * 80 + "\n")