511 lines
18 KiB
Python
511 lines
18 KiB
Python
"""
|
||
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")
|