Spaces:
Sleeping
Sleeping
Download tests/test_logging_properties.py from gagan-devv/CloudOptRL: direct link, hf CLI and curl.
- Browser
- Download file 11.5 kB
-
https://huggingface.co/spaces/gagan-devv/CloudOptRL/resolve/main/tests/test_logging_properties.py
- Command line
-
hf download hf://spaces/gagan-devv/CloudOptRL/tests/test_logging_properties.py
-
curl -L -o test_logging_properties.py https://huggingface.co/spaces/gagan-devv/CloudOptRL/resolve/main/tests/test_logging_properties.py
11.5 kB
| """ | |
| Property-based tests for logging format and clean stdout output. | |
| Tests Properties 5 and 6 from the design document. | |
| """ | |
| import pytest | |
| from hypothesis import given, strategies as st, settings | |
| import re | |
| import sys | |
| import io | |
| from contextlib import redirect_stdout | |
| # Feature: final-submission-compliance, Property 5: Step Log Format Consistency | |
| def test_step_log_format_consistency(step, action, reward, done): | |
| """ | |
| Property 5: [STEP] log matches expected format with 2 decimal places for reward. | |
| **Validates: Requirements 5.2, 5.6** | |
| The [STEP] log should: | |
| - Match format: [STEP] step={step} action={action} reward={reward:.2f} done={done} error={error} | |
| - Format reward with exactly 2 decimal places | |
| """ | |
| # Generate log message using inference script format | |
| error = None | |
| log_message = f"[STEP] step={step} action={action} reward={reward:.2f} done={done} error={error}" | |
| # Verify log format using regex | |
| pattern = r'^\[STEP\] step=\d+ action=[0-2] reward=-?\d+\.\d{2} done=(True|False) error=(None|.+)$' | |
| assert re.match(pattern, log_message), f"Log message '{log_message}' does not match expected format" | |
| # Verify reward has exactly 2 decimal places | |
| reward_match = re.search(r'reward=(-?\d+\.\d{2})', log_message) | |
| assert reward_match is not None, "Reward not found in log message" | |
| # Extract and verify reward value | |
| logged_reward = float(reward_match.group(1)) | |
| expected_reward = round(reward, 2) | |
| assert abs(logged_reward - expected_reward) < 0.01, f"Logged reward {logged_reward} != expected {expected_reward}" | |
| # Feature: final-submission-compliance, Property 5: Step Log Format with Error | |
| def test_step_log_format_with_error(step, action, error_msg): | |
| """ | |
| Property 5: [STEP] log with error matches expected format. | |
| **Validates: Requirements 5.2, 5.6** | |
| The [STEP] log with error should: | |
| - Match format: [STEP] step={step} action={action} reward=0.00 done=True error={error} | |
| - Always show reward=0.00 and done=True when error occurs | |
| """ | |
| # Generate log message for error case | |
| log_message = f"[STEP] step={step} action={action} reward=0.00 done=True error={error_msg}" | |
| # Verify log format | |
| pattern = r'^\[STEP\] step=\d+ action=[0-2] reward=0\.00 done=True error=.+$' | |
| assert re.match(pattern, log_message), f"Error log message '{log_message}' does not match expected format" | |
| # Feature: final-submission-compliance, Property 6: Clean Stdout Output | |
| def test_clean_stdout_output_pattern(): | |
| """ | |
| Property 6: Only [START], [STEP], [END] patterns appear in stdout. | |
| **Validates: Requirements 5.5, 7.1, 7.2, 7.3** | |
| The stdout should: | |
| - Only contain lines matching [START], [STEP], or [END] patterns | |
| - Not contain any debug messages or other output | |
| """ | |
| # Sample valid log output | |
| valid_logs = [ | |
| "[START] task=medium env=cloud_resource_env model=gpt-4", | |
| "[STEP] step=1 action=2 reward=0.85 done=False error=None", | |
| "[STEP] step=2 action=1 reward=0.92 done=False error=None", | |
| "[END] success=True steps=2 score=0.89 rewards=0.85,0.92" | |
| ] | |
| # Verify each log line matches expected patterns | |
| start_pattern = r'^\[START\] task=\w+ env=\w+ model=[\w-]+$' | |
| step_pattern = r'^\[STEP\] step=\d+ action=[0-2] reward=-?\d+\.\d{2} done=(True|False) error=(None|.+)$' | |
| end_pattern = r'^\[END\] success=(True|False) steps=\d+ score=-?\d+\.\d{2} rewards=(-?\d+\.\d{2}(,-?\d+\.\d{2})*)?$' | |
| for log in valid_logs: | |
| is_valid = ( | |
| re.match(start_pattern, log) or | |
| re.match(step_pattern, log) or | |
| re.match(end_pattern, log) | |
| ) | |
| assert is_valid, f"Log line '{log}' does not match any valid pattern" | |
| # Feature: final-submission-compliance, Property 6: Invalid Output Detection | |
| def test_invalid_output_detection(invalid_line): | |
| """ | |
| Property 6: Lines not matching [START], [STEP], [END] are invalid. | |
| **Validates: Requirements 5.5, 7.1, 7.2, 7.3** | |
| Any line that doesn't match the strict log patterns should be detected as invalid. | |
| """ | |
| # Skip if line happens to match valid patterns | |
| start_pattern = r'^\[START\] task=\w+ env=\w+ model=[\w-]+$' | |
| step_pattern = r'^\[STEP\] step=\d+ action=[0-2] reward=-?\d+\.\d{2} done=(True|False) error=(None|.+)$' | |
| end_pattern = r'^\[END\] success=(True|False) steps=\d+ score=-?\d+\.\d{2} rewards=(-?\d+\.\d{2}(,-?\d+\.\d{2})*)?$' | |
| is_valid = ( | |
| re.match(start_pattern, invalid_line) or | |
| re.match(step_pattern, invalid_line) or | |
| re.match(end_pattern, invalid_line) | |
| ) | |
| # If line doesn't match any valid pattern, it should be detected as invalid | |
| if not is_valid: | |
| # This represents a line that should NOT appear in stdout | |
| assert not is_valid, f"Line '{invalid_line}' should be invalid but was not detected" | |
| # Feature: final-submission-compliance, Property 5: START Log Format | |
| def test_start_log_format(task, env, model): | |
| """ | |
| Property 5: [START] log matches expected format. | |
| **Validates: Requirements 5.1** | |
| The [START] log should match format: [START] task={task} env={env} model={model} | |
| """ | |
| # Generate log message | |
| log_message = f"[START] task={task} env={env} model={model}" | |
| # Verify log format | |
| pattern = r'^\[START\] task=[\w-]+ env=[\w-]+ model=[\w-]+$' | |
| assert re.match(pattern, log_message), f"START log '{log_message}' does not match expected format" | |
| # Feature: final-submission-compliance, Property 5: END Log Format | |
| def test_end_log_format(success, steps, score, rewards): | |
| """ | |
| Property 5: [END] log matches expected format. | |
| **Validates: Requirements 5.3, 5.7** | |
| The [END] log should match format: [END] success={success} steps={steps} score={score:.2f} rewards={r1,r2,...} | |
| """ | |
| # Generate log message | |
| rewards_str = ",".join([f"{r:.2f}" for r in rewards]) | |
| log_message = f"[END] success={success} steps={steps} score={score:.2f} rewards={rewards_str}" | |
| # Verify log format | |
| pattern = r'^\[END\] success=(True|False) steps=\d+ score=-?\d+\.\d{2} rewards=(-?\d+\.\d{2}(,-?\d+\.\d{2})*)?$' | |
| assert re.match(pattern, log_message), f"END log '{log_message}' does not match expected format" | |
| # Feature: final-submission-compliance, Property 6: Integration Test for Clean Stdout | |
| def test_inference_script_clean_stdout(): | |
| """ | |
| Property 6: Integration test - inference.py produces only clean stdout. | |
| **Validates: Requirements 5.5, 7.1, 7.2, 7.3, 7.4** | |
| Run the actual inference script and verify: | |
| - Only [START], [STEP], [END] logs appear in stdout | |
| - No debug messages from CloudResourceEnv | |
| - No debug messages from AsyncEnvWrapper | |
| - No FastAPI/uvicorn access logs | |
| """ | |
| import subprocess | |
| # Run inference script and capture stdout | |
| result = subprocess.run( | |
| ["python", "inference.py"], | |
| capture_output=True, | |
| text=True, | |
| timeout=30 | |
| ) | |
| # Verify script completed successfully | |
| assert result.returncode == 0, f"Inference script failed with return code {result.returncode}" | |
| # Get stdout lines | |
| stdout_lines = [line.strip() for line in result.stdout.strip().split('\n') if line.strip()] | |
| # Verify we have output | |
| assert len(stdout_lines) > 0, "No output from inference script" | |
| # Define valid patterns | |
| start_pattern = r'^\[START\] task=\w+ env=\w+ model=[\w-]+$' | |
| step_pattern = r'^\[STEP\] step=\d+ action=[0-2] reward=-?\d+\.\d{2} done=(True|False) error=(None|.+)$' | |
| end_pattern = r'^\[END\] success=(True|False) steps=\d+ score=-?\d+\.\d{2} rewards=(-?\d+\.\d{2}(,-?\d+\.\d{2})*)?$' | |
| # Verify each line matches one of the valid patterns | |
| for i, line in enumerate(stdout_lines): | |
| is_valid = ( | |
| re.match(start_pattern, line) or | |
| re.match(step_pattern, line) or | |
| re.match(end_pattern, line) | |
| ) | |
| assert is_valid, f"Line {i+1} does not match any valid pattern: '{line}'" | |
| # Verify first line is [START] | |
| assert re.match(start_pattern, stdout_lines[0]), f"First line should be [START]: '{stdout_lines[0]}'" | |
| # Verify last line is [END] | |
| assert re.match(end_pattern, stdout_lines[-1]), f"Last line should be [END]: '{stdout_lines[-1]}'" | |
| # Verify no stderr output (except for potential warnings that don't affect stdout) | |
| # We allow stderr to have content, but stdout must be clean | |
| # This is important because errors should go to stderr, not stdout | |
| # Feature: final-submission-compliance, Property 6: Environment and Wrapper Produce No Stdout | |
| def test_environment_and_wrapper_no_stdout(): | |
| """ | |
| Property 6: CloudResourceEnv and AsyncEnvWrapper produce no stdout. | |
| **Validates: Requirements 7.2, 7.3** | |
| Verify that: | |
| - CloudResourceEnv does not print to stdout during normal operation | |
| - AsyncEnvWrapper does not print to stdout during normal operation | |
| """ | |
| import asyncio | |
| from env.environment import CloudResourceEnv | |
| from env.async_wrapper import AsyncEnvWrapper | |
| # Capture stdout | |
| captured_output = io.StringIO() | |
| async def run_episode(): | |
| """Run a short episode and capture any stdout.""" | |
| env = CloudResourceEnv(task_name="medium") | |
| async_env = AsyncEnvWrapper(env) | |
| # Reset environment | |
| await async_env.reset() | |
| # Run a few steps | |
| for _ in range(5): | |
| action = 1 # maintain | |
| obs, reward, done, info = await async_env.step(action) | |
| if done: | |
| break | |
| # Close wrapper | |
| await async_env.close() | |
| # Run episode with stdout capture | |
| with redirect_stdout(captured_output): | |
| asyncio.run(run_episode()) | |
| # Verify no stdout output | |
| output = captured_output.getvalue() | |
| assert output == "", f"Environment/Wrapper produced stdout output: '{output}'" | |
| if __name__ == "__main__": | |
| pytest.main([__file__, "-v"]) | |