""" Tests for the Plugin System Validates hook registration, execution, validation, and integration. """ import pytest from pathlib import Path from typing import Dict, Any, Optional from optimization_engine.plugins import HookManager, Hook, HookPoint from optimization_engine.plugins.validators import ( validate_plugin_code, PluginValidationError, check_hook_function_signature, sanitize_plugin_name, get_imported_modules, estimate_complexity ) class TestHookRegistration: """Test hook registration and management.""" def test_register_simple_hook(self): """Test basic hook registration.""" manager = HookManager() def my_hook(context: Dict[str, Any]) -> Optional[Dict[str, Any]]: return {'status': 'success'} hook = manager.register_hook( hook_point='pre_solve', function=my_hook, description='Test hook', name='test_hook' ) assert hook.name == 'test_hook' assert hook.hook_point == HookPoint.PRE_SOLVE assert hook.enabled is True def test_hook_priority_ordering(self): """Test that hooks execute in priority order.""" manager = HookManager() execution_order = [] def hook_high_priority(context): execution_order.append('high') return None def hook_low_priority(context): execution_order.append('low') return None # Register low priority first, high priority second manager.register_hook('pre_solve', hook_low_priority, 'Low', priority=200) manager.register_hook('pre_solve', hook_high_priority, 'High', priority=10) # Execute hooks manager.execute_hooks('pre_solve', {'trial_number': 1}) # High priority should execute first assert execution_order == ['high', 'low'] def test_disable_enable_hook(self): """Test disabling and enabling hooks.""" manager = HookManager() execution_count = [0] def counting_hook(context): execution_count[0] += 1 return None hook = manager.register_hook( 'pre_solve', counting_hook, 'Counter', name='counter_hook' ) # Execute while enabled manager.execute_hooks('pre_solve', {}) assert execution_count[0] == 1 # Disable and execute manager.disable_hook('counter_hook') manager.execute_hooks('pre_solve', {}) assert execution_count[0] == 1 # Should not increment # Re-enable and execute manager.enable_hook('counter_hook') manager.execute_hooks('pre_solve', {}) assert execution_count[0] == 2 def test_remove_hook(self): """Test hook removal.""" manager = HookManager() def test_hook(context): return None manager.register_hook('pre_solve', test_hook, 'Test', name='removable') assert len(manager.get_hooks('pre_solve')) == 1 success = manager.remove_hook('removable') assert success is True assert len(manager.get_hooks('pre_solve')) == 0 # Try removing non-existent hook success = manager.remove_hook('nonexistent') assert success is False class TestHookExecution: """Test hook execution behavior.""" def test_hook_receives_context(self): """Test that hooks receive correct context.""" manager = HookManager() received_context = {} def context_checker(context: Dict[str, Any]): received_context.update(context) return None manager.register_hook('pre_solve', context_checker, 'Checker') test_context = { 'trial_number': 42, 'design_variables': {'thickness': 5.0}, 'sim_file': 'test.sim' } manager.execute_hooks('pre_solve', test_context) assert received_context['trial_number'] == 42 assert received_context['design_variables']['thickness'] == 5.0 def test_hook_return_values(self): """Test that hook return values are collected.""" manager = HookManager() def hook1(context): return {'result': 'hook1'} def hook2(context): return {'result': 'hook2'} manager.register_hook('pre_solve', hook1, 'Hook 1') manager.register_hook('pre_solve', hook2, 'Hook 2') results = manager.execute_hooks('pre_solve', {}) assert len(results) == 2 assert results[0]['result'] == 'hook1' assert results[1]['result'] == 'hook2' def test_hook_error_handling_fail_fast(self): """Test error handling with fail_fast=True.""" manager = HookManager() def failing_hook(context): raise ValueError("Intentional error") manager.register_hook('pre_solve', failing_hook, 'Failing') with pytest.raises(ValueError, match="Intentional error"): manager.execute_hooks('pre_solve', {}, fail_fast=True) def test_hook_error_handling_continue(self): """Test error handling with fail_fast=False.""" manager = HookManager() execution_log = [] def failing_hook(context): execution_log.append('failing') raise ValueError("Intentional error") def successful_hook(context): execution_log.append('successful') return {'status': 'ok'} manager.register_hook('pre_solve', failing_hook, 'Failing', priority=10) manager.register_hook('pre_solve', successful_hook, 'Success', priority=20) results = manager.execute_hooks('pre_solve', {}, fail_fast=False) # Both hooks should have attempted execution assert execution_log == ['failing', 'successful'] # First result is None (error), second is successful assert results[0] is None assert results[1]['status'] == 'ok' def test_hook_history_tracking(self): """Test that hook execution history is tracked.""" manager = HookManager() def test_hook(context): return {'result': 'success'} manager.register_hook('pre_solve', test_hook, 'Test', name='tracked') # Execute hooks multiple times for i in range(3): manager.execute_hooks('pre_solve', {'trial_number': i}) history = manager.get_history() assert len(history) >= 3 # Check history contains success records successful = [h for h in history if h['success']] assert len(successful) >= 3 class TestCodeValidation: """Test plugin code validation.""" def test_safe_code_passes(self): """Test that safe code passes validation.""" safe_code = """ import numpy as np import math def my_hook(context): x = context['design_variables']['thickness'] result = math.sqrt(x**2 + np.mean([1, 2, 3])) return {'result': result} """ # Should not raise validate_plugin_code(safe_code) def test_dangerous_import_blocked(self): """Test that dangerous imports are blocked.""" dangerous_code = """ import os def my_hook(context): os.system('rm -rf /') return None """ with pytest.raises(PluginValidationError, match="Unsafe import"): validate_plugin_code(dangerous_code) def test_dangerous_operation_blocked(self): """Test that dangerous operations are blocked.""" dangerous_code = """ def my_hook(context): eval('malicious_code') return None """ with pytest.raises(PluginValidationError, match="Dangerous operation"): validate_plugin_code(dangerous_code) def test_file_operations_with_permission(self): """Test that file operations work with allow_file_ops=True.""" code_with_file_ops = """ def my_hook(context): with open('output.txt', 'w') as f: f.write('test') return None """ # Should raise without permission with pytest.raises(PluginValidationError, match="Dangerous operation: open"): validate_plugin_code(code_with_file_ops, allow_file_ops=False) # Should pass with permission validate_plugin_code(code_with_file_ops, allow_file_ops=True) def test_syntax_error_detected(self): """Test that syntax errors are detected.""" bad_syntax = """ def my_hook(context) return None # Missing colon """ with pytest.raises(PluginValidationError, match="Syntax error"): validate_plugin_code(bad_syntax) def test_hook_signature_validation(self): """Test hook function signature validation.""" # Valid signature valid_code = """ def my_hook(context): return None """ assert check_hook_function_signature(valid_code) is True # Invalid: too many arguments invalid_code = """ def my_hook(context, extra_arg): return None """ with pytest.raises(PluginValidationError, match="must take exactly 1 argument"): check_hook_function_signature(invalid_code) # Invalid: no function invalid_code = """ x = 5 """ with pytest.raises(PluginValidationError, match="No function definition"): check_hook_function_signature(invalid_code) class TestUtilityFunctions: """Test plugin utility functions.""" def test_sanitize_plugin_name(self): """Test plugin name sanitization.""" assert sanitize_plugin_name('My-Plugin!') == 'my_plugin_' assert sanitize_plugin_name('123_plugin') == 'plugin_123_plugin' assert sanitize_plugin_name('valid_name') == 'valid_name' def test_get_imported_modules(self): """Test module extraction from code.""" code = """ import numpy as np import math from pathlib import Path from typing import Dict, Any """ modules = get_imported_modules(code) assert 'numpy' in modules assert 'math' in modules assert 'pathlib' in modules assert 'typing' in modules def test_estimate_complexity(self): """Test cyclomatic complexity estimation.""" simple_code = """ def simple(x): return x + 1 """ complex_code = """ def complex(x): if x > 0: for i in range(x): if i % 2 == 0: while i > 0: i -= 1 return x """ simple_complexity = estimate_complexity(simple_code) complex_complexity = estimate_complexity(complex_code) assert simple_complexity == 1 assert complex_complexity > simple_complexity class TestHookManager: """Test HookManager functionality.""" def test_get_summary(self): """Test hook system summary.""" manager = HookManager() def hook1(context): return None def hook2(context): return None manager.register_hook('pre_solve', hook1, 'Hook 1', name='hook1') manager.register_hook('post_solve', hook2, 'Hook 2', name='hook2') manager.disable_hook('hook2') summary = manager.get_summary() assert summary['total_hooks'] == 2 assert summary['enabled_hooks'] == 1 assert summary['disabled_hooks'] == 1 def test_clear_history(self): """Test clearing execution history.""" manager = HookManager() def test_hook(context): return None manager.register_hook('pre_solve', test_hook, 'Test') manager.execute_hooks('pre_solve', {}) assert len(manager.get_history()) > 0 manager.clear_history() assert len(manager.get_history()) == 0 def test_hook_manager_repr(self): """Test HookManager string representation.""" manager = HookManager() def hook(context): return None manager.register_hook('pre_solve', hook, 'Test') repr_str = repr(manager) assert 'HookManager' in repr_str assert 'hooks=1' in repr_str assert 'enabled=1' in repr_str class TestPluginLoading: """Test plugin directory loading.""" def test_load_plugins_from_nonexistent_directory(self): """Test loading from non-existent directory.""" manager = HookManager() # Should not raise, just log warning manager.load_plugins_from_directory(Path('/nonexistent/path')) def test_plugin_registration_function(self): """Test that plugins can register hooks via register_hooks().""" manager = HookManager() # Simulate what a plugin file would contain def register_hooks(hook_manager): def my_plugin_hook(context): return {'plugin': 'loaded'} hook_manager.register_hook( 'pre_solve', my_plugin_hook, 'Plugin hook', name='plugin_hook' ) # Call the registration function register_hooks(manager) # Verify hook was registered hooks = manager.get_hooks('pre_solve') assert len(hooks) == 1 assert hooks[0].name == 'plugin_hook' if __name__ == '__main__': pytest.main([__file__, '-v'])