Files
rpa_vision_v3/tests/property/test_configuration_properties.py
Dom a27b74cf22 v1.0 - Version stable: multi-PC, détection UI-DETR-1, 3 modes exécution
- Frontend v4 accessible sur réseau local (192.168.1.40)
- Ports ouverts: 3002 (frontend), 5001 (backend), 5004 (dashboard)
- Ollama GPU fonctionnel
- Self-healing interactif
- Dashboard confiance

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 11:23:51 +01:00

486 lines
19 KiB
Python

"""
Property-based tests for Configuration Manager
Tests universal correctness properties for the unified configuration system.
Uses real functionality without mocks - tests actual file system operations,
environment variable handling, and configuration validation.
"""
import pytest
import os
import tempfile
import shutil
from pathlib import Path
from hypothesis import given, strategies as st, assume, settings
from hypothesis.stateful import RuleBasedStateMachine, rule, initialize, invariant
from dataclasses import asdict
from typing import Dict, Any
from core.config import (
ConfigurationManager, SystemConfig, ValidationError,
get_configuration_manager, get_config
)
class TestConfigurationProperties:
"""Property tests for Configuration Manager using real functionality"""
def setup_method(self):
"""Setup real temporary directories and clean environment"""
self.temp_dir = Path(tempfile.mkdtemp())
self.original_env = {}
# Store original environment variables we'll modify
env_vars_to_backup = [
"ENVIRONMENT", "API_PORT", "DASHBOARD_PORT", "WORKER_THREADS",
"HEALTH_CHECK_INTERVAL", "SECRET_KEY", "ENCRYPTION_PASSWORD",
"BASE_PATH", "DATA_PATH", "LOGS_PATH"
]
for var in env_vars_to_backup:
self.original_env[var] = os.environ.get(var)
def teardown_method(self):
"""Restore original environment and cleanup real directories"""
# Restore original environment
for key, value in self.original_env.items():
if value is None:
os.environ.pop(key, None)
else:
os.environ[key] = value
# Cleanup real temporary directory
if self.temp_dir.exists():
shutil.rmtree(self.temp_dir)
@given(
environment=st.sampled_from(["development", "staging", "production"]),
api_port=st.integers(min_value=1024, max_value=65535),
dashboard_port=st.integers(min_value=1024, max_value=65535),
worker_threads=st.integers(min_value=1, max_value=32),
health_check_interval=st.integers(min_value=1, max_value=300)
)
@settings(max_examples=50)
def test_property_1_configuration_consistency(
self, environment, api_port, dashboard_port, worker_threads, health_check_interval
):
"""
**Feature: rpa-system-unification, Property 1: Configuration Consistency**
For any system startup, all components should use identical configuration
values for shared settings.
**Validates: Requirements 1.1, 1.2**
"""
assume(api_port != dashboard_port) # Ports must be different
# Set up real environment variables
env_vars = {
"ENVIRONMENT": environment,
"API_PORT": str(api_port),
"DASHBOARD_PORT": str(dashboard_port),
"WORKER_THREADS": str(worker_threads),
"HEALTH_CHECK_INTERVAL": str(health_check_interval),
"BASE_PATH": str(self.temp_dir)
}
# For production environment, add required security keys
if environment == "production":
env_vars["SECRET_KEY"] = "test_production_secret_key_12345"
env_vars["ENCRYPTION_PASSWORD"] = "test_production_encryption_password_12345"
try:
# Set test environment variables
for key, value in env_vars.items():
os.environ[key] = value
# Create multiple configuration manager instances (real objects)
config_manager_1 = ConfigurationManager()
config_manager_2 = ConfigurationManager()
# Load configuration from both managers (real file system operations)
config_1 = config_manager_1.load_config()
config_2 = config_manager_2.load_config()
# Property: All configuration managers should return identical values
assert config_1.environment == config_2.environment == environment
assert config_1.api_port == config_2.api_port == api_port
assert config_1.dashboard_port == config_2.dashboard_port == dashboard_port
assert config_1.worker_threads == config_2.worker_threads == worker_threads
assert config_1.health_check_interval == config_2.health_check_interval == health_check_interval
# Property: Configuration should be consistent across multiple loads
config_1_reload = config_manager_1.reload_config()
assert asdict(config_1) == asdict(config_1_reload)
# Verify real directories were created
assert config_1.data_path.exists()
assert config_1.logs_path.exists()
assert config_1.sessions_path.exists()
# Test real file system permissions
test_file = config_1.data_path / "test_write.txt"
test_file.write_text("test")
assert test_file.read_text() == "test"
test_file.unlink()
finally:
# Environment cleanup handled by teardown_method
pass
@given(
secret_key=st.text(min_size=10, max_size=100),
encryption_password=st.text(min_size=10, max_size=100),
invalid_port=st.integers(max_value=1023) | st.integers(min_value=65536),
invalid_threads=st.integers(max_value=0),
invalid_interval=st.integers(max_value=0)
)
@settings(max_examples=30)
def test_property_10_configuration_validation_completeness(
self, secret_key, encryption_password, invalid_port, invalid_threads, invalid_interval
):
"""
**Feature: rpa-system-unification, Property 10: Configuration Validation Completeness**
For any invalid configuration, the system should detect and report all
validation errors before attempting to start services.
**Validates: Requirements 1.4, 1.5**
"""
# Use real temporary directory for testing
test_base_path = self.temp_dir / "validation_test"
test_base_path.mkdir(exist_ok=True)
config_manager = ConfigurationManager()
# Test production environment validation with real SystemConfig
prod_config = SystemConfig(
base_path=test_base_path,
environment="production",
secret_key="dev_secret_key_not_for_production", # Invalid for production
encryption_password="dev_default_key_not_for_production", # Invalid for production
debug=True # Should warn in production
)
# Real validation using actual validation logic
validation_errors = config_manager.validate_config(prod_config)
# Property: Should detect all production validation errors
error_fields = {error.field for error in validation_errors if error.severity == "error"}
assert "secret_key" in error_fields
assert "encryption_password" in error_fields
warning_fields = {error.field for error in validation_errors if error.severity == "warning"}
assert "debug" in warning_fields
# Test invalid port configuration with real SystemConfig
invalid_port_config = SystemConfig(
base_path=test_base_path,
api_port=invalid_port,
dashboard_port=5001
)
port_errors = config_manager.validate_config(invalid_port_config)
port_error_fields = {error.field for error in port_errors if error.severity == "error"}
assert "api_port" in port_error_fields
# Test same ports configuration
same_ports_config = SystemConfig(
base_path=test_base_path,
api_port=8000,
dashboard_port=8000 # Same as API port
)
same_port_errors = config_manager.validate_config(same_ports_config)
same_port_error_fields = {error.field for error in same_port_errors if error.severity == "error"}
assert "ports" in same_port_error_fields
# Test invalid worker threads
invalid_threads_config = SystemConfig(
base_path=test_base_path,
worker_threads=invalid_threads
)
thread_errors = config_manager.validate_config(invalid_threads_config)
thread_error_fields = {error.field for error in thread_errors if error.severity == "error"}
assert "worker_threads" in thread_error_fields
# Test invalid health check interval
invalid_interval_config = SystemConfig(
base_path=test_base_path,
health_check_interval=invalid_interval
)
interval_errors = config_manager.validate_config(invalid_interval_config)
interval_error_fields = {error.field for error in interval_errors if error.severity == "error"}
assert "health_check_interval" in interval_error_fields
@given(
base_path=st.text(min_size=1, max_size=50).filter(
lambda x: not any(c in x for c in ['/', '\\', ':', '*', '?', '"', '<', '>', '|'])
)
)
@settings(max_examples=20)
def test_configuration_path_resolution(self, base_path):
"""Test that all paths are correctly resolved relative to base_path using real file system"""
# Create real base directory
base_path_obj = self.temp_dir / base_path
base_path_obj.mkdir(exist_ok=True)
# Create real SystemConfig with actual path resolution
config = SystemConfig(base_path=base_path_obj)
# Property: All paths should be absolute and accessible
assert config.data_path.is_absolute()
assert config.logs_path.is_absolute()
assert config.sessions_path.is_absolute()
assert config.workflows_path.is_absolute()
# Test real directory creation
config.ensure_directories()
# Verify directories actually exist on file system
assert config.data_path.exists()
assert config.logs_path.exists()
assert config.sessions_path.exists()
assert config.workflows_path.exists()
# Test real file operations in created directories
test_file = config.data_path / "test.txt"
test_file.write_text("test content")
assert test_file.read_text() == "test content"
def test_configuration_watcher_real_functionality(self):
"""Test that configuration watchers receive real updates using actual callback mechanism"""
# Use real temporary directory
test_base_path = self.temp_dir / "watcher_test"
test_base_path.mkdir(exist_ok=True)
# Set real environment variable
os.environ["BASE_PATH"] = str(test_base_path)
try:
config_manager = ConfigurationManager()
# Track real watcher calls
watcher_calls = []
def test_watcher(config: SystemConfig):
watcher_calls.append({
'environment': config.environment,
'base_path': str(config.base_path),
'api_port': config.api_port
})
# Register real watcher
config_manager.watch_config_changes(test_watcher)
# Load initial configuration (triggers real file system operations)
initial_config = config_manager.load_config()
# Property: Watcher should be called with initial config
assert len(watcher_calls) == 1
assert watcher_calls[0]['environment'] == initial_config.environment
assert Path(watcher_calls[0]['base_path']) == initial_config.base_path
# Apply new configuration with real validation and directory creation
new_config = SystemConfig(
base_path=test_base_path,
environment="staging",
api_port=8001,
dashboard_port=5002
)
config_manager.apply_config(new_config)
# Property: Watcher should be called with new config
assert len(watcher_calls) == 2
assert watcher_calls[1]['environment'] == "staging"
assert watcher_calls[1]['api_port'] == 8001
# Verify real directories were created for new config
assert new_config.data_path.exists()
assert new_config.logs_path.exists()
finally:
# Cleanup handled by teardown_method
pass
def test_real_environment_variable_loading(self):
"""Test loading configuration from real environment variables"""
# Set real environment variables
test_env = {
"BASE_PATH": str(self.temp_dir),
"ENVIRONMENT": "staging",
"API_PORT": "8080",
"DASHBOARD_PORT": "5050",
"WORKER_THREADS": "8",
"CLIP_MODEL": "ViT-L-14",
"FAISS_DIMENSIONS": "768"
}
try:
for key, value in test_env.items():
os.environ[key] = value
# Load configuration using real environment variable parsing
config_manager = ConfigurationManager()
config = config_manager.load_config()
# Verify real environment variables were loaded correctly
assert config.environment == "staging"
assert config.api_port == 8080
assert config.dashboard_port == 5050
assert config.worker_threads == 8
assert config.clip_model == "ViT-L-14"
assert config.faiss_dimensions == 768
assert config.base_path == self.temp_dir
# Verify real directories were created
assert config.data_path.exists()
assert config.logs_path.exists()
finally:
# Cleanup environment variables
for key in test_env:
os.environ.pop(key, None)
class ConfigurationStateMachine(RuleBasedStateMachine):
"""Stateful property testing for Configuration Manager using real functionality"""
def __init__(self):
super().__init__()
self.temp_dir = Path(tempfile.mkdtemp())
self.config_manager = ConfigurationManager()
self.applied_configs = []
self.watcher_calls = []
self.original_env = {}
# Backup original environment
for var in ["BASE_PATH", "ENVIRONMENT", "API_PORT", "DASHBOARD_PORT"]:
self.original_env[var] = os.environ.get(var)
# Set base path to our temporary directory
os.environ["BASE_PATH"] = str(self.temp_dir)
def teardown(self):
"""Cleanup real resources"""
# Restore environment
for key, value in self.original_env.items():
if value is None:
os.environ.pop(key, None)
else:
os.environ[key] = value
# Cleanup real directory
if self.temp_dir.exists():
shutil.rmtree(self.temp_dir)
@initialize()
def setup(self):
"""Initialize with real configuration manager and watcher"""
def track_watcher(config: SystemConfig):
self.watcher_calls.append({
'environment': config.environment,
'timestamp': len(self.watcher_calls)
})
self.config_manager.watch_config_changes(track_watcher)
@rule(
environment=st.sampled_from(["development", "staging"]),
api_port=st.integers(min_value=8000, max_value=8010),
dashboard_port=st.integers(min_value=5000, max_value=5010)
)
def apply_valid_config(self, environment, api_port, dashboard_port):
"""Apply a valid configuration using real SystemConfig and file operations"""
assume(api_port != dashboard_port)
# Create real configuration
config = SystemConfig(
base_path=self.temp_dir,
environment=environment,
api_port=api_port,
dashboard_port=dashboard_port
)
# Apply using real configuration manager
self.config_manager.apply_config(config)
self.applied_configs.append(config)
# Verify real directories were created
assert config.data_path.exists()
assert config.logs_path.exists()
@rule()
def reload_config(self):
"""Reload configuration from real environment"""
reloaded = self.config_manager.reload_config()
# Property: Reloaded config should be valid using real validation
errors = self.config_manager.validate_config(reloaded)
error_count = sum(1 for error in errors if error.severity == "error")
assert error_count == 0
# Verify real directories exist
assert reloaded.data_path.exists()
@invariant()
def config_always_valid(self):
"""Configuration should always be valid using real validation logic"""
current_config = self.config_manager.get_config()
errors = self.config_manager.validate_config(current_config)
error_count = sum(1 for error in errors if error.severity == "error")
assert error_count == 0
@invariant()
def watchers_called_for_changes(self):
"""Watchers should be called for each real configuration change"""
# Number of watcher calls should match number of config changes + initial load
expected_calls = len(self.applied_configs) + 1 # +1 for initial load
assert len(self.watcher_calls) >= expected_calls
@invariant()
def directories_always_exist(self):
"""Real directories should always exist for current configuration"""
current_config = self.config_manager.get_config()
assert current_config.data_path.exists()
assert current_config.logs_path.exists()
# Test the stateful machine
TestConfigurationStateMachine = ConfigurationStateMachine.TestCase
if __name__ == "__main__":
# Run a quick property test with real functionality
test_instance = TestConfigurationProperties()
test_instance.setup_method()
try:
print("Running Property 1: Configuration Consistency with real file system...")
test_instance.test_property_1_configuration_consistency(
environment="development",
api_port=8000,
dashboard_port=5001,
worker_threads=4,
health_check_interval=30
)
print("✅ Property 1 passed")
print("Running Property 10: Configuration Validation with real validation...")
test_instance.test_property_10_configuration_validation_completeness(
secret_key="test_key_12345",
encryption_password="test_password_12345",
invalid_port=80,
invalid_threads=-1,
invalid_interval=0
)
print("✅ Property 10 passed")
print("Running real environment variable loading test...")
test_instance.test_real_environment_variable_loading()
print("✅ Environment variable loading test passed")
print("✅ All configuration properties validated with real functionality")
finally:
test_instance.teardown_method()