CloudOptRL / tests /test_inference_properties.py
gagan-devv's picture
Initial submission
cb922bf
Raw History Blame Contribute Delete
6.72 kB
"""
Property-based tests for inference script.
Tests Properties 3, 4, 7, 8, 9 from the design document.
"""
import pytest
from hypothesis import given, strategies as st, settings
import numpy as np
import os
import sys
from unittest.mock import Mock
# Import inference script components
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from inference import heuristic_policy, MAX_STEPS, SUCCESS_SCORE_THRESHOLD
# Feature: final-submission-compliance, Property 4: Heuristic Policy Correctness
@given(cpu=st.floats(min_value=0.0, max_value=100.0, allow_nan=False, allow_infinity=False))
@settings(max_examples=100)
def test_heuristic_policy_correctness(cpu):
"""
Property 4: For any CPU value, heuristic policy returns correct action.
**Validates: Requirements 4.1, 4.2, 4.3, 4.5**
The heuristic policy should:
- Return action 2 (increase) when CPU > 70%
- Return action 0 (decrease) when CPU < 40%
- Return action 1 (maintain) when CPU is in [40%, 70%]
- Always return an integer in {0, 1, 2}
"""
# Create mock state with CPU value
state = Mock()
state.cpu = cpu
# Get action from heuristic policy
action = heuristic_policy(state)
# Verify action is valid integer
assert isinstance(action, int), f"Action must be integer, got {type(action)}"
assert action in {0, 1, 2}, f"Action must be in {{0, 1, 2}}, got {action}"
# Verify correct action for CPU value
if cpu > 70.0:
assert action == 2, f"CPU {cpu:.2f}% > 70% should return action 2 (increase), got {action}"
elif cpu < 40.0:
assert action == 0, f"CPU {cpu:.2f}% < 40% should return action 0 (decrease), got {action}"
else:
assert action == 1, f"CPU {cpu:.2f}% in [40%, 70%] should return action 1 (maintain), got {action}"
# Feature: final-submission-compliance, Property 7: Average Reward Calculation
@given(rewards=st.lists(st.floats(min_value=-10.0, max_value=10.0, allow_nan=False, allow_infinity=False), min_size=0, max_size=50))
@settings(max_examples=100)
def test_average_reward_calculation(rewards):
"""
Property 7: For any list of rewards, average equals sum/len or 0.0 if empty.
**Validates: Requirements 6.3**
The average reward calculation should:
- Return sum(rewards) / len(rewards) for non-empty lists
- Return 0.0 for empty lists
"""
# Calculate average using inference script logic
avg_reward = sum(rewards) / len(rewards) if rewards else 0.0
# Verify calculation
if len(rewards) == 0:
assert avg_reward == 0.0, "Empty reward list should have average 0.0"
else:
expected_avg = sum(rewards) / len(rewards)
assert abs(avg_reward - expected_avg) < 1e-6, f"Average {avg_reward} != expected {expected_avg}"
# Feature: final-submission-compliance, Property 8: Success Determination
@given(avg_reward=st.floats(min_value=-10.0, max_value=10.0, allow_nan=False, allow_infinity=False))
@settings(max_examples=100)
def test_success_determination(avg_reward):
"""
Property 8: Success is True if and only if average reward >= 0.5.
**Validates: Requirements 6.4, 6.5**
The success determination should:
- Return True when avg_reward >= SUCCESS_SCORE_THRESHOLD (0.5)
- Return False when avg_reward < SUCCESS_SCORE_THRESHOLD (0.5)
"""
# Calculate success using inference script logic
success = avg_reward >= SUCCESS_SCORE_THRESHOLD
# Verify success determination
if avg_reward >= SUCCESS_SCORE_THRESHOLD:
assert success is True, f"avg_reward {avg_reward:.2f} >= {SUCCESS_SCORE_THRESHOLD} should be success=True"
else:
assert success is False, f"avg_reward {avg_reward:.2f} < {SUCCESS_SCORE_THRESHOLD} should be success=False"
# Feature: final-submission-compliance, Property 9: Episode Termination
@given(
steps_taken=st.integers(min_value=0, max_value=MAX_STEPS + 10),
done_flags=st.lists(st.booleans(), min_size=0, max_size=MAX_STEPS + 10)
)
@settings(max_examples=100)
def test_episode_termination(steps_taken, done_flags):
"""
Property 9: Episode terminates at MAX_STEPS or when done=True.
**Validates: Requirements 6.6**
The episode should terminate when:
- MAX_STEPS (20) is reached, OR
- Environment returns done=True
"""
# Simulate episode termination logic
terminated = False
actual_steps = 0
for step in range(min(steps_taken, MAX_STEPS)):
actual_steps = step + 1
# Check if done flag is True at this step
if step < len(done_flags) and done_flags[step]:
terminated = True
break
# Check if MAX_STEPS reached
if actual_steps >= MAX_STEPS:
terminated = True
break
# Verify termination conditions
if actual_steps >= MAX_STEPS:
assert terminated, f"Episode should terminate at MAX_STEPS={MAX_STEPS}"
if any(done_flags[:actual_steps]):
assert terminated, "Episode should terminate when done=True"
# Feature: final-submission-compliance, Property 3: Environment Variable Defaults
def test_environment_variable_defaults():
"""
Property 3: Environment variables default to correct values when not set.
**Validates: Requirements 3.4, 3.5**
The inference script should use:
- TASK_NAME defaults to "medium" when not set
- BENCHMARK defaults to "cloud_resource_env" when not set
"""
# Save original environment variables
original_task = os.environ.get("TASK_NAME")
original_benchmark = os.environ.get("BENCHMARK")
try:
# Clear environment variables
if "TASK_NAME" in os.environ:
del os.environ["TASK_NAME"]
if "BENCHMARK" in os.environ:
del os.environ["BENCHMARK"]
# Re-import to get fresh values
import importlib
import inference
importlib.reload(inference)
# Verify defaults
assert inference.TASK_NAME == "medium", f"TASK_NAME should default to 'medium', got '{inference.TASK_NAME}'"
assert inference.BENCHMARK == "cloud_resource_env", f"BENCHMARK should default to 'cloud_resource_env', got '{inference.BENCHMARK}'"
finally:
# Restore original environment variables
if original_task is not None:
os.environ["TASK_NAME"] = original_task
if original_benchmark is not None:
os.environ["BENCHMARK"] = original_benchmark
# Reload to restore original state
importlib.reload(inference)
if __name__ == "__main__":
pytest.main([__file__, "-v"])