| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265 |
- # -*- coding: utf-8 -*-
- """
- Tests für Satellite-Extensions.
- Testet:
- - Groups: SatelliteGroup, GroupManager
- - Zones: Zone, ZoneManager
- - LoadBalancing: Strategien und LoadBalancer
- - Health: HealthScorer, HealthMetrics, Thresholds
- - Failover: FailoverManager, BackupRegistry
- """
- import asyncio
- from datetime import datetime, timedelta
- from unittest.mock import MagicMock, AsyncMock
- import pytest
- # ============================================================================
- # Mock-Satellite für Tests
- # ============================================================================
- class MockSatellite:
- """Mock-Satellite für Tests."""
- def __init__(
- self,
- satellite_id: str,
- room_id: str = "default",
- is_connected: bool = True,
- ):
- self.id = satellite_id
- self.room_id = room_id
- self.is_connected = is_connected
- self.last_heartbeat = datetime.now()
- self.active_requests = 0
- self.response_time_ms = 50.0
- def __repr__(self) -> str:
- return f"MockSatellite(id={self.id!r})"
- # ============================================================================
- # Group Tests
- # ============================================================================
- class TestSatelliteGroup:
- """Tests für SatelliteGroup."""
- def test_create_group(self):
- """Test Gruppe erstellen."""
- from trixy_core.satellite.groups import SatelliteGroup, GroupType
- group = SatelliteGroup(
- group_id="test_group",
- name="Test Gruppe",
- group_type=GroupType.CUSTOM
- )
- assert group.id == "test_group"
- assert group.name == "Test Gruppe"
- assert group.group_type == GroupType.CUSTOM
- assert group.size == 0
- assert group.is_empty
- def test_add_satellite(self):
- """Test Satellite hinzufügen."""
- from trixy_core.satellite.groups import SatelliteGroup
- group = SatelliteGroup("test")
- sat = MockSatellite("sat1")
- assert group.add(sat) is True
- assert group.size == 1
- assert not group.is_empty
- assert group.contains("sat1")
- # Duplikat verhindern
- assert group.add(sat) is False
- assert group.size == 1
- def test_remove_satellite(self):
- """Test Satellite entfernen."""
- from trixy_core.satellite.groups import SatelliteGroup
- group = SatelliteGroup("test")
- sat = MockSatellite("sat1")
- group.add(sat)
- removed = group.remove("sat1")
- assert removed is sat
- assert group.size == 0
- # Nicht vorhanden
- assert group.remove("not_found") is None
- def test_max_members(self):
- """Test maximale Mitglieder."""
- from trixy_core.satellite.groups import SatelliteGroup, GroupConfig
- config = GroupConfig(max_members=2)
- group = SatelliteGroup("test", config=config)
- assert group.add(MockSatellite("sat1")) is True
- assert group.add(MockSatellite("sat2")) is True
- assert group.add(MockSatellite("sat3")) is False
- assert group.size == 2
- def test_filter_satellites(self):
- """Test Satellite-Filter."""
- from trixy_core.satellite.groups import SatelliteGroup
- group = SatelliteGroup("test")
- group.add(MockSatellite("sat1", is_connected=True))
- group.add(MockSatellite("sat2", is_connected=False))
- group.add(MockSatellite("sat3", is_connected=True))
- connected = group.filter(lambda s: s.is_connected)
- assert len(connected) == 2
- disconnected = group.filter(lambda s: not s.is_connected)
- assert len(disconnected) == 1
- def test_get_connected(self):
- """Test verbundene Satellites."""
- from trixy_core.satellite.groups import SatelliteGroup
- group = SatelliteGroup("test")
- group.add(MockSatellite("sat1", is_connected=True))
- group.add(MockSatellite("sat2", is_connected=False))
- connected = group.get_connected()
- assert len(connected) == 1
- assert connected[0].id == "sat1"
- def test_group_criteria(self):
- """Test automatische Zuordnung mit Kriterium."""
- from trixy_core.satellite.groups import SatelliteGroup
- group = SatelliteGroup(
- "room_test",
- criteria=lambda s: s.room_id == "wohnzimmer"
- )
- sat1 = MockSatellite("sat1", room_id="wohnzimmer")
- sat2 = MockSatellite("sat2", room_id="küche")
- assert group.matches(sat1) is True
- assert group.matches(sat2) is False
- def test_contains_operator(self):
- """Test __contains__ Operator."""
- from trixy_core.satellite.groups import SatelliteGroup
- group = SatelliteGroup("test")
- sat = MockSatellite("sat1")
- group.add(sat)
- assert "sat1" in group
- assert sat in group
- assert "sat2" not in group
- def test_iteration(self):
- """Test Iteration über Gruppe."""
- from trixy_core.satellite.groups import SatelliteGroup
- group = SatelliteGroup("test")
- group.add(MockSatellite("sat1"))
- group.add(MockSatellite("sat2"))
- ids = [s.id for s in group]
- assert "sat1" in ids
- assert "sat2" in ids
- def test_to_dict(self):
- """Test Serialisierung."""
- from trixy_core.satellite.groups import SatelliteGroup, GroupType
- group = SatelliteGroup("test", "Test Gruppe", GroupType.ROOM)
- group.add(MockSatellite("sat1"))
- data = group.to_dict()
- assert data["id"] == "test"
- assert data["name"] == "Test Gruppe"
- assert data["type"] == "ROOM"
- assert "sat1" in data["members"]
- class TestGroupManager:
- """Tests für GroupManager."""
- def test_create_group(self):
- """Test Gruppe erstellen."""
- from trixy_core.satellite.groups import GroupManager, GroupType
- manager = GroupManager()
- group = manager.create("test", "Test Gruppe", GroupType.CUSTOM)
- assert group.id == "test"
- assert manager.count == 1
- assert "test" in manager
- def test_create_duplicate_fails(self):
- """Test Duplikat-Erstellung verhindert."""
- from trixy_core.satellite.groups import GroupManager
- manager = GroupManager()
- manager.create("test")
- with pytest.raises(ValueError):
- manager.create("test")
- def test_create_room_group(self):
- """Test Raum-Gruppe erstellen."""
- from trixy_core.satellite.groups import GroupManager, GroupType
- manager = GroupManager()
- group = manager.create_room_group("wohnzimmer", "Wohnzimmer")
- assert group.group_type == GroupType.ROOM
- assert group.id == "room:wohnzimmer"
- # Über room_id abrufen
- assert manager.get_by_room("wohnzimmer") is group
- def test_create_zone_group(self):
- """Test Zone-Gruppe erstellen."""
- from trixy_core.satellite.groups import GroupManager, GroupType
- manager = GroupManager()
- group = manager.create_zone_group(
- "erdgeschoss",
- "Erdgeschoss",
- rooms=["wohnzimmer", "küche"]
- )
- assert group.group_type == GroupType.ZONE
- assert group.id == "zone:erdgeschoss"
- # Kriterium testen
- sat1 = MockSatellite("sat1", room_id="wohnzimmer")
- sat2 = MockSatellite("sat2", room_id="bad")
- assert group.matches(sat1) is True
- assert group.matches(sat2) is False
- def test_auto_assign(self):
- """Test automatische Zuordnung."""
- from trixy_core.satellite.groups import GroupManager
- manager = GroupManager()
- manager.create_room_group("wohnzimmer")
- manager.create_room_group("küche")
- sat = MockSatellite("sat1", room_id="wohnzimmer")
- assigned = manager.auto_assign(sat)
- assert "room:wohnzimmer" in assigned
- assert "room:küche" not in assigned
- def test_get_groups_for_satellite(self):
- """Test Gruppen für Satellite abrufen."""
- from trixy_core.satellite.groups import GroupManager
- manager = GroupManager()
- g1 = manager.create("group1")
- g2 = manager.create("group2")
- sat = MockSatellite("sat1")
- g1.add(sat)
- g2.add(sat)
- groups = manager.get_groups_for_satellite("sat1")
- assert len(groups) == 2
- def test_delete_group(self):
- """Test Gruppe löschen."""
- from trixy_core.satellite.groups import GroupManager
- manager = GroupManager()
- manager.create("test")
- deleted = manager.delete("test")
- assert deleted is not None
- assert "test" not in manager
- def test_find_groups(self):
- """Test Gruppen suchen."""
- from trixy_core.satellite.groups import GroupManager, GroupType
- manager = GroupManager()
- manager.create("g1", group_type=GroupType.ROOM)
- g2 = manager.create("g2", group_type=GroupType.ZONE)
- g2.add(MockSatellite("sat1", is_connected=True))
- # Nach Typ filtern
- rooms = manager.find_groups(group_type=GroupType.ROOM)
- assert len(rooms) == 1
- # Mit verbundenen Satellites
- with_connected = manager.find_groups(has_connected=True)
- assert len(with_connected) == 1
- # ============================================================================
- # Zone Tests
- # ============================================================================
- class TestZone:
- """Tests für Zone."""
- def test_create_zone(self):
- """Test Zone erstellen."""
- from trixy_core.satellite.groups import Zone
- zone = Zone(
- zone_id="erdgeschoss",
- name="Erdgeschoss",
- rooms=["wohnzimmer", "küche"]
- )
- assert zone.id == "erdgeschoss"
- assert zone.name == "Erdgeschoss"
- assert "wohnzimmer" in zone.rooms
- assert "küche" in zone.rooms
- def test_add_remove_room(self):
- """Test Raum hinzufügen/entfernen."""
- from trixy_core.satellite.groups import Zone
- zone = Zone("test")
- zone.add_room("flur")
- assert "flur" in zone.rooms
- zone.remove_room("flur")
- assert "flur" not in zone.rooms
- def test_zone_hierarchy(self):
- """Test Zone-Hierarchie."""
- from trixy_core.satellite.groups import Zone
- parent = Zone("haus", "Gesamtes Haus")
- child = Zone("erdgeschoss", "Erdgeschoss", rooms=["wohnzimmer"])
- parent.add_child(child)
- assert child.parent is parent
- assert child in parent.children
- assert child.depth == 1
- assert parent.depth == 0
- assert parent.is_root
- assert not child.is_root
- def test_all_rooms_recursive(self):
- """Test alle Räume rekursiv."""
- from trixy_core.satellite.groups import Zone
- parent = Zone("haus", rooms=["garage"])
- child1 = Zone("eg", rooms=["wohnzimmer", "küche"])
- child2 = Zone("og", rooms=["schlafzimmer", "bad"])
- parent.add_child(child1)
- parent.add_child(child2)
- all_rooms = parent.all_rooms
- assert "garage" in all_rooms
- assert "wohnzimmer" in all_rooms
- assert "schlafzimmer" in all_rooms
- assert len(all_rooms) == 5
- def test_contains_room_recursive(self):
- """Test contains_room rekursiv."""
- from trixy_core.satellite.groups import Zone
- parent = Zone("haus")
- child = Zone("eg", rooms=["wohnzimmer"])
- parent.add_child(child)
- assert parent.contains_room("wohnzimmer", recursive=True)
- assert not parent.contains_room("wohnzimmer", recursive=False)
- assert "wohnzimmer" in parent # __contains__ ist rekursiv
- def test_get_path(self):
- """Test Pfad zu Zone."""
- from trixy_core.satellite.groups import Zone
- root = Zone("haus")
- level1 = Zone("eg")
- level2 = Zone("wohnbereich")
- root.add_child(level1)
- level1.add_child(level2)
- path = level2.get_path()
- assert len(path) == 3
- assert path[0] is root
- assert path[2] is level2
- class TestZoneManager:
- """Tests für ZoneManager."""
- def test_create_zone(self):
- """Test Zone erstellen."""
- from trixy_core.satellite.groups import ZoneManager
- manager = ZoneManager()
- zone = manager.create("test", "Test Zone", rooms=["raum1"])
- assert zone.id == "test"
- assert manager.count == 1
- assert "test" in manager
- def test_create_with_parent(self):
- """Test Zone mit Parent erstellen."""
- from trixy_core.satellite.groups import ZoneManager
- manager = ZoneManager()
- parent = manager.create("parent")
- child = manager.create("child", parent="parent")
- assert child.parent is parent
- assert child in parent.children
- def test_invalid_parent_raises(self):
- """Test ungültiger Parent wirft Fehler."""
- from trixy_core.satellite.groups import ZoneManager
- manager = ZoneManager()
- with pytest.raises(ValueError):
- manager.create("child", parent="nonexistent")
- def test_get_zone_for_room(self):
- """Test Zone für Raum finden."""
- from trixy_core.satellite.groups import ZoneManager
- manager = ZoneManager()
- zone = manager.create("eg", rooms=["wohnzimmer", "küche"])
- found = manager.get_zone_for_room("wohnzimmer")
- assert found is zone
- not_found = manager.get_zone_for_room("garage")
- assert not_found is None
- def test_move_room(self):
- """Test Raum verschieben."""
- from trixy_core.satellite.groups import ZoneManager
- manager = ZoneManager()
- z1 = manager.create("z1", rooms=["raum1"])
- z2 = manager.create("z2")
- assert manager.move_room("raum1", "z2") is True
- assert "raum1" not in z1.rooms
- assert "raum1" in z2.rooms
- def test_delete_zone(self):
- """Test Zone löschen."""
- from trixy_core.satellite.groups import ZoneManager
- manager = ZoneManager()
- manager.create("test")
- deleted = manager.delete("test")
- assert deleted is not None
- assert "test" not in manager
- def test_delete_recursive(self):
- """Test rekursives Löschen."""
- from trixy_core.satellite.groups import ZoneManager
- manager = ZoneManager()
- manager.create("parent")
- manager.create("child1", parent="parent")
- manager.create("child2", parent="parent")
- manager.delete("parent", recursive=True)
- assert manager.count == 0
- def test_get_hierarchy(self):
- """Test Hierarchie abrufen."""
- from trixy_core.satellite.groups import ZoneManager
- manager = ZoneManager()
- manager.create("root1", rooms=["r1"])
- manager.create("child", parent="root1", rooms=["r2"])
- manager.create("root2", rooms=["r3"])
- hierarchy = manager.get_hierarchy()
- assert len(hierarchy["root_zones"]) == 2
- # ============================================================================
- # LoadBalancing Tests
- # ============================================================================
- class TestLoadBalancingStrategy:
- """Tests für Load-Balancing-Strategien."""
- def test_round_robin(self):
- """Test Round-Robin-Strategie."""
- from trixy_core.satellite.loadbalancing import RoundRobinStrategy
- strategy = RoundRobinStrategy()
- satellites = [
- MockSatellite("sat1"),
- MockSatellite("sat2"),
- MockSatellite("sat3"),
- ]
- # Sollte in Reihenfolge durchgehen
- results = []
- for _ in range(6):
- result = strategy.select(satellites)
- results.append(result.satellite.id)
- # Jeder Satellite sollte 2x vorkommen
- assert results.count("sat1") == 2
- assert results.count("sat2") == 2
- assert results.count("sat3") == 2
- def test_round_robin_handles_removed_satellite(self):
- """Test Round-Robin bei entferntem Satellite."""
- from trixy_core.satellite.loadbalancing import RoundRobinStrategy
- strategy = RoundRobinStrategy()
- satellites = [MockSatellite("sat1"), MockSatellite("sat2")]
- strategy.select(satellites) # sat1
- # Satellite entfernen
- satellites = [MockSatellite("sat2")]
- result = strategy.select(satellites)
- assert result.satellite.id == "sat2"
- def test_round_robin_empty_list(self):
- """Test Round-Robin mit leerer Liste."""
- from trixy_core.satellite.loadbalancing import RoundRobinStrategy
- strategy = RoundRobinStrategy()
- result = strategy.select([])
- assert result is None
- def test_least_connections(self):
- """Test Least-Connections-Strategie."""
- from trixy_core.satellite.loadbalancing import LeastConnectionsStrategy
- strategy = LeastConnectionsStrategy()
- satellites = [
- MockSatellite("sat1"),
- MockSatellite("sat2"),
- MockSatellite("sat3"),
- ]
- # Initial alle gleich, erstes ausgewählt
- result1 = strategy.select(satellites)
- strategy.on_request_start(result1.satellite)
- # Jetzt sollte sat2 oder sat3 gewählt werden (weniger Connections)
- result2 = strategy.select(satellites)
- assert result2.satellite.id != "sat1"
- def test_least_connections_end_request(self):
- """Test Least-Connections Connection-Tracking."""
- from trixy_core.satellite.loadbalancing import LeastConnectionsStrategy
- strategy = LeastConnectionsStrategy()
- sat = MockSatellite("sat1")
- strategy.on_request_start(sat)
- assert strategy.get_connection_count("sat1") == 1
- strategy.on_request_end(sat)
- assert strategy.get_connection_count("sat1") == 0
- def test_weighted_strategy(self):
- """Test Weighted-Strategie."""
- from trixy_core.satellite.loadbalancing import WeightedStrategy, WeightConfig
- strategy = WeightedStrategy()
- # Hohe Gewichtung für sat1
- strategy.set_weight("sat1", WeightConfig(weight=10.0))
- strategy.set_weight("sat2", WeightConfig(weight=1.0))
- satellites = [MockSatellite("sat1"), MockSatellite("sat2")]
- # sat1 sollte deutlich öfter gewählt werden
- counts = {"sat1": 0, "sat2": 0}
- for _ in range(100):
- result = strategy.select(satellites)
- counts[result.satellite.id] += 1
- # sat1 sollte ca. 90% bekommen
- assert counts["sat1"] > counts["sat2"] * 5
- def test_weighted_zero_weight(self):
- """Test Weighted mit Gewicht 0."""
- from trixy_core.satellite.loadbalancing import WeightedStrategy, WeightConfig
- strategy = WeightedStrategy()
- strategy.set_weight("sat1", WeightConfig(weight=0.0))
- strategy.set_weight("sat2", WeightConfig(weight=1.0))
- satellites = [MockSatellite("sat1"), MockSatellite("sat2")]
- # Nur sat2 sollte gewählt werden
- for _ in range(10):
- result = strategy.select(satellites)
- assert result.satellite.id == "sat2"
- class TestLoadBalancer:
- """Tests für LoadBalancer."""
- def test_load_balancer_select(self):
- """Test LoadBalancer Auswahl."""
- from trixy_core.satellite.loadbalancing import (
- LoadBalancer,
- RoundRobinStrategy
- )
- balancer = LoadBalancer(RoundRobinStrategy())
- satellites = [MockSatellite("sat1"), MockSatellite("sat2")]
- result = balancer.select(satellites)
- assert result is not None
- assert result.satellite.id in ["sat1", "sat2"]
- def test_load_balancer_filters_disconnected(self):
- """Test LoadBalancer filtert nicht verbundene automatisch."""
- from trixy_core.satellite.loadbalancing import (
- LoadBalancer,
- RoundRobinStrategy
- )
- balancer = LoadBalancer(RoundRobinStrategy())
- satellites = [
- MockSatellite("sat1", is_connected=False),
- MockSatellite("sat2", is_connected=True),
- ]
- # LoadBalancer filtert disconnected automatisch
- result = balancer.select(satellites)
- assert result.satellite.id == "sat2"
- def test_load_balancer_change_strategy(self):
- """Test Strategie-Wechsel."""
- from trixy_core.satellite.loadbalancing import (
- LoadBalancer,
- RoundRobinStrategy,
- LeastConnectionsStrategy
- )
- balancer = LoadBalancer(RoundRobinStrategy())
- assert balancer.strategy.name == "round_robin"
- # Strategie über Property-Setter ändern
- balancer.strategy = LeastConnectionsStrategy()
- assert balancer.strategy.name == "least_connections"
- # ============================================================================
- # Health Tests
- # ============================================================================
- class TestHealthMetrics:
- """Tests für HealthMetrics."""
- def test_create_metrics(self):
- """Test Metriken erstellen."""
- from trixy_core.satellite.health import HealthMetrics
- metrics = HealthMetrics(
- latency_ms=50.0,
- packet_loss=0.01,
- uptime=0.99,
- )
- assert metrics.latency_ms == 50.0
- assert metrics.packet_loss == 0.01
- assert metrics.uptime == 0.99
- def test_custom_metrics(self):
- """Test benutzerdefinierte Metriken."""
- from trixy_core.satellite.health import HealthMetrics
- metrics = HealthMetrics()
- metrics.custom_metrics["audio_quality"] = 0.95
- assert metrics.custom_metrics["audio_quality"] == 0.95
- class TestHealthScorer:
- """Tests für HealthScorer."""
- def test_calculate_score(self):
- """Test Score-Berechnung."""
- from trixy_core.satellite.health import HealthScorer, HealthMetrics
- scorer = HealthScorer()
- metrics = HealthMetrics(
- latency_ms=50.0,
- packet_loss=0.0,
- uptime=1.0,
- error_rate=0.0,
- )
- # Metriken zuerst speichern
- scorer.update_metrics("sat1", metrics)
- score = scorer.calculate_score("sat1")
- # Bei perfekten Werten sollte Score hoch sein
- assert score.overall > 80
- def test_health_state_mapping(self):
- """Test Health-State-Zuordnung."""
- from trixy_core.satellite.health import (
- HealthScorer,
- HealthMetrics,
- HealthState
- )
- scorer = HealthScorer()
- # Gute Metriken -> HEALTHY
- good = HealthMetrics(latency_ms=20, packet_loss=0, uptime=1.0)
- scorer.update_metrics("sat_good", good)
- score = scorer.calculate_score("sat_good")
- assert score.state == HealthState.HEALTHY
- # Mittelmäßige Metriken -> DEGRADED
- degraded = HealthMetrics(latency_ms=500, packet_loss=0.2, uptime=0.5)
- scorer.update_metrics("sat_degraded", degraded)
- score = scorer.calculate_score("sat_degraded")
- assert score.state in [HealthState.DEGRADED, HealthState.UNHEALTHY]
- # Sehr schlechte Metriken -> CRITICAL oder UNHEALTHY
- critical = HealthMetrics(
- latency_ms=1000,
- packet_loss=0.5,
- uptime=0.3,
- error_rate=0.5,
- last_heartbeat_age=120.0,
- )
- scorer.update_metrics("sat_critical", critical)
- score = scorer.calculate_score("sat_critical")
- assert score.state in [HealthState.CRITICAL, HealthState.UNHEALTHY]
- def test_custom_weights(self):
- """Test benutzerdefinierte Gewichtung."""
- from trixy_core.satellite.health import HealthScorer, HealthMetrics
- scorer = HealthScorer(weights={
- "latency": 0.8,
- "packet_loss": 0.1,
- "uptime": 0.1,
- })
- metrics = HealthMetrics(latency_ms=200, packet_loss=0.0, uptime=1.0)
- scorer.update_metrics("sat1", metrics)
- score = scorer.calculate_score("sat1")
- # Mit hoher Latenz-Gewichtung sollte Score niedriger sein
- assert score.overall < 80
- def test_score_history(self):
- """Test Score-Historie."""
- from trixy_core.satellite.health import HealthScorer, HealthMetrics
- scorer = HealthScorer(history_size=5)
- for i in range(3):
- metrics = HealthMetrics(latency_ms=50 + i * 10)
- scorer.update_metrics("sat1", metrics)
- scorer.calculate_score("sat1")
- history = scorer.get_history("sat1")
- assert len(history) == 3
- class TestHealthThresholds:
- """Tests für HealthThresholds."""
- def test_default_thresholds(self):
- """Test Standard-Schwellenwerte."""
- from trixy_core.satellite.health import HealthThresholds
- thresholds = HealthThresholds()
- # Sollte Default-Configs haben
- assert thresholds.get("latency_ms") is not None
- assert thresholds.get("packet_loss") is not None
- def test_add_threshold(self):
- """Test Schwellenwert hinzufügen."""
- from trixy_core.satellite.health import (
- HealthThresholds,
- ThresholdConfig
- )
- thresholds = HealthThresholds(use_defaults=False)
- config = ThresholdConfig(
- metric_name="custom_metric",
- warning_threshold=50.0,
- error_threshold=80.0,
- )
- thresholds.add(config)
- assert thresholds.get("custom_metric") is config
- def test_check_violations(self):
- """Test Schwellenwert-Verletzung erkennen."""
- from trixy_core.satellite.health import (
- HealthThresholds,
- HealthMetrics,
- ThresholdConfig,
- ViolationSeverity
- )
- thresholds = HealthThresholds(use_defaults=False)
- thresholds.add(ThresholdConfig(
- metric_name="latency_ms",
- warning_threshold=100.0,
- error_threshold=200.0,
- critical_threshold=500.0,
- ))
- # Über Warning-Schwelle
- metrics = HealthMetrics(latency_ms=150.0)
- violations = thresholds.check("sat1", metrics)
- assert len(violations) == 1
- assert violations[0].severity == ViolationSeverity.WARNING
- def test_violation_callback(self):
- """Test Callback bei Verletzung."""
- from trixy_core.satellite.health import (
- HealthThresholds,
- HealthMetrics,
- ThresholdConfig
- )
- thresholds = HealthThresholds(use_defaults=False)
- thresholds.add(ThresholdConfig(
- metric_name="latency_ms",
- warning_threshold=100.0,
- ))
- violations_received = []
- @thresholds.on_violation
- def handler(violation):
- violations_received.append(violation)
- metrics = HealthMetrics(latency_ms=150.0)
- thresholds.check("sat1", metrics)
- assert len(violations_received) == 1
- def test_cooldown(self):
- """Test Cooldown zwischen Alarmen."""
- from trixy_core.satellite.health import (
- HealthThresholds,
- HealthMetrics,
- ThresholdConfig
- )
- thresholds = HealthThresholds(use_defaults=False)
- thresholds.add(ThresholdConfig(
- metric_name="latency_ms",
- warning_threshold=100.0,
- cooldown=300.0, # 5 Minuten
- ))
- metrics = HealthMetrics(latency_ms=150.0)
- # Erste Prüfung: Verletzung
- v1 = thresholds.check("sat1", metrics)
- assert len(v1) == 1
- # Zweite Prüfung: Cooldown aktiv, keine Verletzung
- v2 = thresholds.check("sat1", metrics)
- assert len(v2) == 0
- # Cooldown zurücksetzen
- thresholds.clear_cooldowns("sat1")
- v3 = thresholds.check("sat1", metrics)
- assert len(v3) == 1
- # ============================================================================
- # Failover Tests
- # ============================================================================
- class TestBackupRegistry:
- """Tests für BackupRegistry."""
- def test_assign_backup(self):
- """Test Backup zuweisen."""
- from trixy_core.satellite.failover import BackupRegistry
- registry = BackupRegistry()
- assignment = registry.assign("primary1", "backup1", priority=10)
- assert assignment.primary_id == "primary1"
- assert assignment.backup_id == "backup1"
- assert assignment.priority == 10
- def test_assign_self_fails(self):
- """Test Self-Assignment verhindert."""
- from trixy_core.satellite.failover import BackupRegistry
- registry = BackupRegistry()
- with pytest.raises(ValueError):
- registry.assign("sat1", "sat1")
- def test_multiple_backups_priority(self):
- """Test mehrere Backups nach Priorität."""
- from trixy_core.satellite.failover import BackupRegistry
- registry = BackupRegistry()
- registry.assign("primary", "backup1", priority=5)
- registry.assign("primary", "backup2", priority=10)
- registry.assign("primary", "backup3", priority=1)
- backups = registry.get_backups("primary")
- # Sollte nach Priorität sortiert sein
- assert backups[0].backup_id == "backup2" # priority=10
- assert backups[1].backup_id == "backup1" # priority=5
- assert backups[2].backup_id == "backup3" # priority=1
- def test_get_best_backup(self):
- """Test bestes Backup abrufen."""
- from trixy_core.satellite.failover import BackupRegistry
- def get_satellite(sat_id):
- if sat_id == "backup1":
- return MockSatellite(sat_id, is_connected=False)
- return MockSatellite(sat_id, is_connected=True)
- registry = BackupRegistry(get_satellite=get_satellite)
- registry.assign("primary", "backup1", priority=10) # disconnected
- registry.assign("primary", "backup2", priority=5) # connected
- best = registry.get_best_backup("primary")
- # backup1 hat höhere Priorität aber ist disconnected
- assert best.backup_id == "backup2"
- def test_unassign_backup(self):
- """Test Backup-Zuweisung entfernen."""
- from trixy_core.satellite.failover import BackupRegistry
- registry = BackupRegistry()
- registry.assign("primary", "backup1")
- registry.assign("primary", "backup2")
- removed = registry.unassign("primary", "backup1")
- assert removed == 1
- backups = registry.get_backups("primary")
- assert len(backups) == 1
- def test_unassign_all(self):
- """Test alle Backups entfernen."""
- from trixy_core.satellite.failover import BackupRegistry
- registry = BackupRegistry()
- registry.assign("primary", "backup1")
- registry.assign("primary", "backup2")
- removed = registry.unassign("primary")
- assert removed == 2
- assert len(registry.get_backups("primary")) == 0
- def test_backup_strategies(self):
- """Test Backup-Strategien."""
- from trixy_core.satellite.failover import (
- BackupRegistry,
- BackupStrategy
- )
- def get_all():
- return [
- MockSatellite("sat1", room_id="room1"),
- MockSatellite("sat2", room_id="room1"),
- MockSatellite("sat3", room_id="room2"),
- ]
- def get_sat(sat_id):
- sats = {s.id: s for s in get_all()}
- return sats.get(sat_id)
- registry = BackupRegistry(
- default_strategy=BackupStrategy.SAME_ROOM,
- get_satellite=get_sat,
- get_all_satellites=get_all,
- )
- # sat1 sucht Backup in room1
- backup = registry.get_best_backup("sat1")
- assert backup is not None
- assert backup.backup_id == "sat2" # Im selben Raum
- def test_update_health_score(self):
- """Test Health-Score für Backup-Auswahl."""
- from trixy_core.satellite.failover import (
- BackupRegistry,
- BackupStrategy
- )
- satellites = [
- MockSatellite("sat1"),
- MockSatellite("sat2"),
- MockSatellite("sat3"),
- ]
- registry = BackupRegistry(
- default_strategy=BackupStrategy.HEALTH_BASED,
- get_satellite=lambda id: next((s for s in satellites if s.id == id), None),
- get_all_satellites=lambda: satellites,
- )
- registry.update_health_score("sat2", 90.0)
- registry.update_health_score("sat3", 50.0)
- backup = registry.get_best_backup("sat1")
- assert backup.backup_id == "sat2" # Höherer Health-Score
- class TestFailoverManager:
- """Tests für FailoverManager."""
- @pytest.fixture
- def setup_manager(self):
- """Fixture für FailoverManager Setup."""
- from trixy_core.satellite.failover import (
- FailoverManager,
- FailoverConfig,
- BackupRegistry
- )
- satellites = {
- "primary1": MockSatellite("primary1"),
- "backup1": MockSatellite("backup1"),
- "backup2": MockSatellite("backup2"),
- }
- registry = BackupRegistry(
- get_satellite=lambda id: satellites.get(id),
- get_all_satellites=lambda: list(satellites.values()),
- )
- registry.assign("primary1", "backup1", priority=10)
- registry.assign("primary1", "backup2", priority=5)
- manager = FailoverManager(
- backup_registry=registry,
- config=FailoverConfig(detection_timeout=5.0),
- get_satellite=lambda id: satellites.get(id),
- )
- return manager, satellites
- @pytest.mark.asyncio
- async def test_trigger_failover(self, setup_manager):
- """Test Failover auslösen."""
- from trixy_core.satellite.failover import FailoverState
- manager, _ = setup_manager
- event = await manager.trigger_failover(
- "primary1",
- reason="Test-Failover"
- )
- assert event is not None
- assert event.primary_id == "primary1"
- assert event.backup_id == "backup1" # Höchste Priorität
- assert event.state == FailoverState.ACTIVE
- @pytest.mark.asyncio
- async def test_failover_no_backup_available(self, setup_manager):
- """Test Failover ohne verfügbares Backup."""
- from trixy_core.satellite.failover import FailoverState
- manager, satellites = setup_manager
- # Beide Backups disconnecten
- satellites["backup1"].is_connected = False
- satellites["backup2"].is_connected = False
- event = await manager.trigger_failover("primary1")
- assert event.state == FailoverState.FAILED
- assert "Kein Backup" in event.reason
- @pytest.mark.asyncio
- async def test_failover_callback(self, setup_manager):
- """Test Failover-Callback."""
- manager, _ = setup_manager
- events_received = []
- @manager.on_failover
- async def handler(event):
- events_received.append(event)
- await manager.trigger_failover("primary1")
- assert len(events_received) == 1
- @pytest.mark.asyncio
- async def test_recover(self, setup_manager):
- """Test Recovery nach Failover."""
- from trixy_core.satellite.failover import FailoverState
- manager, satellites = setup_manager
- # Failover auslösen
- await manager.trigger_failover("primary1")
- assert "primary1" in manager.active_failovers
- # Recovery
- success = await manager.recover("primary1")
- assert success is True
- assert "primary1" not in manager.active_failovers
- @pytest.mark.asyncio
- async def test_prevent_duplicate_failover(self, setup_manager):
- """Test verhindert doppeltes Failover."""
- manager, _ = setup_manager
- event1 = await manager.trigger_failover("primary1")
- event2 = await manager.trigger_failover("primary1")
- # Sollte das gleiche Event zurückgeben
- assert event1 is event2
- @pytest.mark.asyncio
- async def test_get_history(self, setup_manager):
- """Test Failover-Historie."""
- manager, _ = setup_manager
- await manager.trigger_failover("primary1", reason="Test 1")
- await manager.recover("primary1")
- history = manager.get_history()
- assert len(history) >= 1
- def test_get_statistics(self, setup_manager):
- """Test Statistiken."""
- manager, _ = setup_manager
- stats = manager.get_statistics()
- assert "total_failovers" in stats
- assert "successful" in stats
- assert "failed" in stats
- assert "success_rate" in stats
- # ============================================================================
- # Integration Tests
- # ============================================================================
- class TestSatelliteExtensionsIntegration:
- """Integration-Tests für alle Satellite-Extensions."""
- def test_groups_with_loadbalancing(self):
- """Test Gruppen mit Load-Balancing."""
- from trixy_core.satellite.groups import SatelliteGroup
- from trixy_core.satellite.loadbalancing import (
- LoadBalancer,
- RoundRobinStrategy
- )
- group = SatelliteGroup("test")
- group.add(MockSatellite("sat1"))
- group.add(MockSatellite("sat2"))
- group.add(MockSatellite("sat3"))
- balancer = LoadBalancer(RoundRobinStrategy())
- # Load-Balancing über Gruppe
- result = balancer.select(group.get_connected())
- assert result is not None
- def test_health_triggers_failover(self):
- """Test Health löst Failover aus."""
- from trixy_core.satellite.health import (
- HealthScorer,
- HealthMetrics,
- HealthState
- )
- from trixy_core.satellite.failover import (
- FailoverManager,
- FailoverConfig,
- BackupRegistry
- )
- # Health Score berechnen
- scorer = HealthScorer()
- bad_metrics = HealthMetrics(
- latency_ms=600,
- packet_loss=0.15,
- error_rate=0.1,
- )
- scorer.update_metrics("sat1", bad_metrics)
- score = scorer.calculate_score("sat1")
- # Bei kritischem Zustand sollte Failover möglich sein
- if score.state in [HealthState.CRITICAL, HealthState.UNHEALTHY]:
- registry = BackupRegistry()
- manager = FailoverManager(
- backup_registry=registry,
- config=FailoverConfig(health_threshold=40.0),
- )
- # Überprüfung: Score liegt unter Threshold?
- assert score.overall < manager.config.health_threshold or score.state != HealthState.HEALTHY
- def test_zone_hierarchy_with_groups(self):
- """Test Zone-Hierarchie mit Gruppen."""
- from trixy_core.satellite.groups import (
- Zone,
- ZoneManager,
- GroupManager
- )
- # Zonen erstellen
- zones = ZoneManager()
- zones.create("haus", "Gesamtes Haus")
- zones.create("eg", "Erdgeschoss", rooms=["wohnzimmer", "küche"], parent="haus")
- zones.create("og", "Obergeschoss", rooms=["schlafzimmer"], parent="haus")
- # Gruppen für Räume
- groups = GroupManager()
- groups.create_room_group("wohnzimmer")
- groups.create_room_group("küche")
- # Satellites zuweisen
- sat1 = MockSatellite("sat1", room_id="wohnzimmer")
- sat2 = MockSatellite("sat2", room_id="küche")
- groups.auto_assign(sat1)
- groups.auto_assign(sat2)
- # Zone sollte beide Räume enthalten
- eg = zones.get("eg")
- assert "wohnzimmer" in eg
- assert "küche" in eg
- if __name__ == "__main__":
- pytest.main([__file__, "-v"])
|