Files
rpa_vision_v3/tests/property/test_realtime_validation_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

523 lines
20 KiB
Python

#!/usr/bin/env python3
"""
Tests de Propriété pour RealtimeValidationService - RPA Vision V3
Ces tests vérifient les propriétés universelles du service de validation en temps réel
en utilisant des tests basés sur les propriétés avec Hypothesis.
Feature: visual-rpa-properties-enhancement
Property 14: Validation Périodique Automatique
Property 15: Récupération Intelligente d'Éléments
Auteur: Assistant IA
Date: 2026-01-07
"""
import pytest
import asyncio
import threading
import time
from datetime import datetime, timedelta
from unittest.mock import Mock, AsyncMock, patch
from hypothesis import given, strategies as st, assume, settings
import numpy as np
from core.visual.realtime_validation_service import (
RealtimeValidationService,
ValidationStatus,
ValidationResult,
ValidationConfig
)
from core.visual.visual_target_manager import VisualTarget
from core.models import UIElement, BBox
class TestRealtimeValidationServiceProperties:
"""Tests de propriété pour RealtimeValidationService"""
@pytest.fixture
def mock_dependencies(self):
"""Fixture pour créer les dépendances mockées"""
screen_capturer = Mock()
screen_capturer.capture_screen = AsyncMock()
ui_detector = Mock()
ui_detector.detect_elements = AsyncMock()
embedding_manager = Mock()
embedding_manager.find_best_match = AsyncMock()
target_manager = Mock()
target_manager.update_target_screenshot = AsyncMock()
return {
'screen_capturer': screen_capturer,
'ui_detector': ui_detector,
'embedding_manager': embedding_manager,
'target_manager': target_manager
}
@pytest.fixture
def validation_service(self, mock_dependencies):
"""Fixture pour créer le service de validation"""
return RealtimeValidationService(**mock_dependencies)
@pytest.fixture
def sample_visual_target(self):
"""Fixture pour créer une cible visuelle de test"""
return VisualTarget(
embedding=np.random.rand(256).astype(np.float32),
screenshot="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
bounding_box=BoundingBox(x=100, y=100, width=50, height=30),
confidence=0.85,
signature="test_signature_123",
metadata=Mock(),
contextual_info=Mock()
)
@given(
validation_interval=st.floats(min_value=0.1, max_value=10.0),
num_validations=st.integers(min_value=1, max_value=10)
)
@settings(max_examples=20, deadline=10000)
def test_property_14_periodic_automatic_validation(
self,
validation_service,
sample_visual_target,
validation_interval,
num_validations
):
"""
Property 14: Validation Périodique Automatique
Pour tout élément configuré, le système doit vérifier périodiquement
sa présence et afficher des indicateurs de statut appropriés.
Valide: Exigences 6.1, 6.2, 6.3
"""
element_id = "test_element_periodic"
validation_results = []
def validation_callback(result):
validation_results.append(result)
# Configuration de validation
config = ValidationConfig(
target=sample_visual_target,
validation_interval=validation_interval,
callback=validation_callback
)
# Mock des réponses de validation
mock_screen_state = Mock()
mock_screen_state.ui_elements = [Mock()]
validation_service.screen_capturer.capture_screen.return_value = Mock()
validation_service.ui_detector.detect_elements.return_value = mock_screen_state
# Simuler des résultats de validation variables
match_results = []
for i in range(num_validations):
match_result = Mock()
match_result.confidence = 0.8 + (i % 3) * 0.1
match_result.element = Mock()
match_results.append(match_result)
validation_service.embedding_manager.find_best_match.side_effect = match_results
# Démarrer la validation
success = validation_service.start_validation(element_id, sample_visual_target, config)
assert success
# Attendre plusieurs cycles de validation
expected_validations = min(num_validations, 3) # Limiter pour les tests
wait_time = validation_interval * expected_validations + 0.5
time.sleep(wait_time)
# Arrêter la validation
validation_service.stop_validation(element_id)
# Vérifier que des validations ont eu lieu
assert len(validation_results) >= 1
# Vérifier que toutes les validations ont des propriétés valides
for result in validation_results:
assert isinstance(result, ValidationResult)
assert result.status in [ValidationStatus.VALID, ValidationStatus.WARNING, ValidationStatus.ERROR]
assert 0.0 <= result.confidence <= 1.0
assert isinstance(result.timestamp, datetime)
# Vérifier que les validations sont espacées correctement
if len(validation_results) > 1:
time_diffs = []
for i in range(1, len(validation_results)):
diff = (validation_results[i].timestamp - validation_results[i-1].timestamp).total_seconds()
time_diffs.append(diff)
# Les intervalles doivent être proches de l'intervalle configuré (±50%)
for diff in time_diffs:
assert validation_interval * 0.5 <= diff <= validation_interval * 2.0
@given(
confidence_threshold=st.floats(min_value=0.1, max_value=0.9),
initial_confidence=st.floats(min_value=0.0, max_value=1.0),
recovery_confidence=st.floats(min_value=0.0, max_value=1.0)
)
@settings(max_examples=25, deadline=5000)
def test_property_15_intelligent_element_recovery(
self,
validation_service,
sample_visual_target,
confidence_threshold,
initial_confidence,
recovery_confidence
):
"""
Property 15: Récupération Intelligente d'Éléments
Pour tout élément qui change d'apparence ou disparaît, le système doit
proposer des actions de récupération (mise à jour ou re-sélection).
Valide: Exigences 6.4, 6.5
"""
assume(initial_confidence != recovery_confidence) # Assurer un changement
element_id = "test_element_recovery"
validation_results = []
def validation_callback(result):
validation_results.append(result)
# Configuration avec récupération automatique
config = ValidationConfig(
target=sample_visual_target,
validation_interval=0.5,
confidence_threshold=confidence_threshold,
auto_recovery=True,
callback=validation_callback
)
# Mock des dépendances
mock_screen_state = Mock()
mock_screen_state.ui_elements = [Mock()]
validation_service.screen_capturer.capture_screen.return_value = Mock()
validation_service.ui_detector.detect_elements.return_value = mock_screen_state
# Simuler un scénario de récupération
match_results = []
# Premier résultat avec confiance initiale
first_match = Mock() if initial_confidence > 0 else None
if first_match:
first_match.confidence = initial_confidence
first_match.element = Mock()
match_results.append(first_match)
# Deuxième résultat avec confiance de récupération
second_match = Mock() if recovery_confidence > 0 else None
if second_match:
second_match.confidence = recovery_confidence
second_match.element = Mock()
match_results.append(second_match)
validation_service.embedding_manager.find_best_match.side_effect = match_results
# Mock des méthodes de récupération
validation_service.target_manager.update_target_screenshot.return_value = sample_visual_target
# Démarrer la validation
success = validation_service.start_validation(element_id, sample_visual_target, config)
assert success
# Attendre les validations
time.sleep(1.5) # Permettre au moins 2 validations
# Arrêter la validation
validation_service.stop_validation(element_id)
# Analyser les résultats
assert len(validation_results) >= 1
# Vérifier la logique de récupération
for result in validation_results:
if result.confidence < confidence_threshold:
# Si la confiance est faible, des actions de récupération doivent être proposées
assert len(result.recovery_actions) > 0 or len(result.suggestions) > 0
# Les actions de récupération doivent être appropriées
valid_actions = ['re_select', 'update_target', 'expand_search']
for action in result.recovery_actions:
assert action in valid_actions
# Vérifier que le statut correspond à la confiance
if result.confidence >= confidence_threshold:
assert result.status in [ValidationStatus.VALID, ValidationStatus.WARNING]
else:
assert result.status == ValidationStatus.ERROR
@given(
num_concurrent_validations=st.integers(min_value=1, max_value=5),
validation_duration=st.floats(min_value=0.5, max_value=2.0)
)
@settings(max_examples=15, deadline=8000)
def test_property_concurrent_validation_safety(
self,
validation_service,
num_concurrent_validations,
validation_duration
):
"""
Propriété: Sécurité des Validations Concurrentes
Pour tout ensemble de validations simultanées, le service doit
maintenir la cohérence des données sans corruption.
"""
element_ids = [f"element_{i}" for i in range(num_concurrent_validations)]
all_results = {eid: [] for eid in element_ids}
# Créer des cibles uniques pour chaque élément
targets = []
for i in range(num_concurrent_validations):
target = VisualTarget(
embedding=np.random.rand(256).astype(np.float32),
screenshot=f"screenshot_{i}",
bounding_box=BoundingBox(x=i*100, y=i*50, width=50, height=30),
confidence=0.8,
signature=f"signature_{i}",
metadata=Mock(),
contextual_info=Mock()
)
targets.append(target)
# Callbacks pour collecter les résultats
def create_callback(element_id):
def callback(result):
all_results[element_id].append(result)
return callback
# Mock des dépendances
mock_screen_state = Mock()
mock_screen_state.ui_elements = [Mock() for _ in range(num_concurrent_validations)]
validation_service.screen_capturer.capture_screen.return_value = Mock()
validation_service.ui_detector.detect_elements.return_value = mock_screen_state
# Mock des résultats de matching
def mock_find_best_match(embedding, candidates):
# Retourner un résultat basé sur l'embedding
match = Mock()
match.confidence = 0.7 + (hash(str(embedding)) % 3) * 0.1
match.element = Mock()
return match
validation_service.embedding_manager.find_best_match.side_effect = mock_find_best_match
# Démarrer toutes les validations
started_validations = []
for i, element_id in enumerate(element_ids):
config = ValidationConfig(
target=targets[i],
validation_interval=0.3,
callback=create_callback(element_id)
)
success = validation_service.start_validation(element_id, targets[i], config)
if success:
started_validations.append(element_id)
# Vérifier que toutes les validations ont démarré
assert len(started_validations) == num_concurrent_validations
# Attendre la durée de validation
time.sleep(validation_duration)
# Arrêter toutes les validations
for element_id in started_validations:
validation_service.stop_validation(element_id)
# Vérifier l'intégrité des résultats
for element_id in started_validations:
results = all_results[element_id]
# Chaque validation doit avoir produit au moins un résultat
assert len(results) >= 1
# Vérifier que tous les résultats sont valides
for result in results:
assert isinstance(result, ValidationResult)
assert result.status in ValidationStatus
assert 0.0 <= result.confidence <= 1.0
# Vérifier qu'il n'y a pas de corruption croisée
all_timestamps = []
for element_id in started_validations:
for result in all_results[element_id]:
all_timestamps.append((element_id, result.timestamp))
# Les timestamps doivent être cohérents (pas de doublons exacts)
timestamp_values = [ts for _, ts in all_timestamps]
assert len(timestamp_values) == len(set(timestamp_values))
@given(
max_retries=st.integers(min_value=1, max_value=5),
failure_rate=st.floats(min_value=0.0, max_value=1.0)
)
@settings(max_examples=20, deadline=5000)
def test_property_retry_mechanism(
self,
validation_service,
sample_visual_target,
max_retries,
failure_rate
):
"""
Propriété: Mécanisme de Retry
Pour tout échec de validation, le système doit respecter
le nombre maximum de tentatives configuré.
"""
element_id = "test_element_retry"
validation_attempts = []
def validation_callback(result):
validation_attempts.append(result)
# Configuration avec retry
config = ValidationConfig(
target=sample_visual_target,
validation_interval=0.2,
max_retries=max_retries,
callback=validation_callback
)
# Mock pour simuler des échecs selon le taux configuré
call_count = 0
def mock_find_best_match(embedding, candidates):
nonlocal call_count
call_count += 1
# Simuler un échec selon le taux de failure
if np.random.random() < failure_rate:
return None # Échec de matching
else:
match = Mock()
match.confidence = 0.8
match.element = Mock()
return match
# Setup des mocks
mock_screen_state = Mock()
mock_screen_state.ui_elements = [Mock()]
validation_service.screen_capturer.capture_screen.return_value = Mock()
validation_service.ui_detector.detect_elements.return_value = mock_screen_state
validation_service.embedding_manager.find_best_match.side_effect = mock_find_best_match
# Démarrer la validation
success = validation_service.start_validation(element_id, sample_visual_target, config)
assert success
# Attendre suffisamment pour permettre les retries
wait_time = config.validation_interval * (max_retries + 2)
time.sleep(wait_time)
# Arrêter la validation
validation_service.stop_validation(element_id)
# Analyser les tentatives
assert len(validation_attempts) >= 1
# Compter les échecs consécutifs
consecutive_failures = 0
max_consecutive_failures = 0
for result in validation_attempts:
if result.status == ValidationStatus.ERROR:
consecutive_failures += 1
max_consecutive_failures = max(max_consecutive_failures, consecutive_failures)
else:
consecutive_failures = 0
# Le nombre d'échecs consécutifs ne doit pas dépasser max_retries
# (sauf si le taux d'échec est très élevé)
if failure_rate < 0.9: # Si le taux d'échec n'est pas trop élevé
assert max_consecutive_failures <= max_retries + 1
def test_property_validation_result_consistency(self, validation_service, sample_visual_target):
"""
Propriété: Cohérence des Résultats de Validation
Pour tout résultat de validation, les propriétés doivent
être cohérentes et respecter les contraintes logiques.
"""
element_id = "test_element_consistency"
validation_results = []
def validation_callback(result):
validation_results.append(result)
config = ValidationConfig(
target=sample_visual_target,
validation_interval=0.3,
confidence_threshold=0.7,
callback=validation_callback
)
# Mock avec différents scénarios
scenarios = [
(0.9, ValidationStatus.VALID), # Haute confiance
(0.75, ValidationStatus.VALID), # Confiance acceptable
(0.65, ValidationStatus.ERROR), # Confiance faible
(0.0, ValidationStatus.ERROR), # Aucune confiance
]
mock_results = []
for confidence, expected_status in scenarios:
if confidence > 0:
match = Mock()
match.confidence = confidence
match.element = Mock()
mock_results.append(match)
else:
mock_results.append(None)
# Setup des mocks
mock_screen_state = Mock()
mock_screen_state.ui_elements = [Mock()]
validation_service.screen_capturer.capture_screen.return_value = Mock()
validation_service.ui_detector.detect_elements.return_value = mock_screen_state
validation_service.embedding_manager.find_best_match.side_effect = mock_results
# Démarrer et attendre
success = validation_service.start_validation(element_id, sample_visual_target, config)
assert success
time.sleep(1.5) # Permettre plusieurs validations
validation_service.stop_validation(element_id)
# Vérifier la cohérence des résultats
for result in validation_results:
# Cohérence status/confiance
if result.confidence >= config.confidence_threshold:
assert result.status in [ValidationStatus.VALID, ValidationStatus.WARNING]
else:
assert result.status == ValidationStatus.ERROR
# Cohérence des suggestions/actions
if result.status == ValidationStatus.ERROR:
assert len(result.issues) > 0 or len(result.suggestions) > 0
# Propriétés temporelles
assert isinstance(result.timestamp, datetime)
assert result.timestamp <= datetime.now()
# Propriétés numériques
assert 0.0 <= result.confidence <= 1.0
if __name__ == "__main__":
pytest.main([__file__, "-v", "--tb=short"])