Skip to content

Commit 296f36c

Browse files
committed
improvement(nemesis_tests): Rework nemesis tests
- Move them to separate directory - Use custom subclass hierarchies to make them less dependent on the implementation - Add NemesisRegistry tests - Fixes #10592
1 parent d5ed8a0 commit 296f36c

File tree

6 files changed

+445
-346
lines changed

6 files changed

+445
-346
lines changed

unit_tests/nemesis/__init__.py

Whitespace-only changes.

unit_tests/nemesis/fake_cluster.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Mock classes"""
2+
3+
from dataclasses import dataclass, field
4+
5+
from sdcm.cluster import BaseScyllaCluster
6+
from unit_tests.dummy_remote import LocalLoaderSetDummy
7+
8+
9+
PARAMS = dict(nemesis_interval=1, nemesis_filter_seeds=False)
10+
11+
12+
@dataclass
13+
class Node:
14+
running_nemesis = None
15+
public_ip_address: str = '127.0.0.1'
16+
name: str = 'Node1'
17+
18+
@property
19+
def scylla_shards(self):
20+
return 8
21+
22+
def log_message(self, *args, **kwargs):
23+
pass
24+
25+
26+
@dataclass
27+
class Cluster:
28+
nodes: list
29+
params: dict = field(default_factory=lambda: PARAMS)
30+
31+
def check_cluster_health(self):
32+
pass
33+
34+
@property
35+
def data_nodes(self):
36+
return self.nodes
37+
38+
@property
39+
def zero_nodes(self):
40+
return self.nodes
41+
42+
def log_message(self, *args, **kwargs):
43+
pass
44+
45+
46+
@dataclass
47+
class FakeTester:
48+
params: dict = field(default_factory=lambda: PARAMS)
49+
loaders: LocalLoaderSetDummy = field(default_factory=LocalLoaderSetDummy)
50+
db_cluster: Cluster | BaseScyllaCluster = field(default_factory=lambda: Cluster(nodes=[Node(), Node()]))
51+
monitors: list = field(default_factory=list)
52+
53+
def __post_init__(self):
54+
self.db_cluster.params = self.params
55+
56+
def create_stats(self):
57+
pass
58+
59+
def update(self, *args, **kwargs):
60+
pass
61+
62+
def get_scylla_versions(self):
63+
pass
64+
65+
def get_test_details(self):
66+
pass
67+
68+
def id(self):
69+
return 0

unit_tests/nemesis/test_registry.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
"""
2+
This module tests NemesisRegistry method on custom Subclass tree
3+
Should not be dependent on the implementation of Nemesis class
4+
"""
5+
6+
import pytest
7+
from sdcm.nemesis_registry import NemesisRegistry
8+
9+
10+
class TestNemesis:
11+
COMMON_STRING = "called test function "
12+
flag_a = False
13+
flag_b = False
14+
flag_c = False
15+
flag_d = False
16+
flag_common = False
17+
18+
def disrupt_method_a(self):
19+
print(self.COMMON_STRING + "a")
20+
21+
def disrupt_method_b(self):
22+
print(self.COMMON_STRING + "b")
23+
24+
def disrupt_method_c(self):
25+
print(self.COMMON_STRING + "c")
26+
27+
def disrupt_method_d(self):
28+
print(self.COMMON_STRING + "d")
29+
30+
31+
class CustomNemesisA(TestNemesis):
32+
flag_a = True
33+
flag_common = True
34+
35+
def disrupt(self):
36+
self.disrupt_method_a()
37+
38+
39+
class CustomNemesisB(TestNemesis):
40+
flag_b = True
41+
flag_common = True
42+
43+
def disrupt(self):
44+
self.disrupt_method_b()
45+
46+
47+
class CustomNemesisC(CustomNemesisA):
48+
flag_c = True
49+
50+
def disrupt(self):
51+
self.disrupt_method_c()
52+
53+
54+
class CustomNemesisD(CustomNemesisB):
55+
flag_d = True
56+
flag_a = True
57+
flag_common = True
58+
59+
def disrupt(self):
60+
self.disrupt_method_d()
61+
62+
63+
@pytest.fixture
64+
def registry():
65+
return NemesisRegistry(base_class=TestNemesis)
66+
67+
68+
@pytest.mark.parametrize(
69+
"logical_phrase, expected_classes",
70+
[
71+
("flag_a", {CustomNemesisA, CustomNemesisD}),
72+
("flag_b", {CustomNemesisB}),
73+
("flag_c", {CustomNemesisC}),
74+
("flag_d", {CustomNemesisD}),
75+
("flag_common", {CustomNemesisA, CustomNemesisB, CustomNemesisD}),
76+
],
77+
)
78+
def test_filter_subclasses_by_single_flag(registry, logical_phrase, expected_classes):
79+
filtered = registry.filter_subclasses(registry.get_subclasses(), logical_phrase)
80+
assert set(filtered) == expected_classes
81+
82+
83+
@pytest.mark.parametrize(
84+
"logical_phrase, expected_classes",
85+
[
86+
("flag_a and flag_d", {CustomNemesisD}),
87+
("flag_common and not flag_c", {CustomNemesisA, CustomNemesisB, CustomNemesisD}),
88+
("CustomNemesisA or flag_d", {CustomNemesisA, CustomNemesisD}),
89+
],
90+
)
91+
def test_filter_subclasses_by_combined_flags(registry, logical_phrase, expected_classes):
92+
filtered = registry.filter_subclasses(registry.get_subclasses(), logical_phrase)
93+
assert set(filtered) == expected_classes
94+
95+
96+
@pytest.mark.parametrize(
97+
"logical_phrase, expected_classes",
98+
[
99+
("(CustomNemesisA or CustomNemesisD) and not flag_d", {CustomNemesisA}),
100+
("flag_common and not flag_a", {CustomNemesisB}),
101+
],
102+
)
103+
def test_filter_subclasses_with_complex_expression(registry, logical_phrase, expected_classes):
104+
filtered = registry.filter_subclasses(registry.get_subclasses(), logical_phrase)
105+
assert set(filtered) == expected_classes
106+
107+
108+
def test_get_subclasses(registry):
109+
subclasses = registry.get_subclasses()
110+
assert set(subclasses) == {CustomNemesisA, CustomNemesisB, CustomNemesisC, CustomNemesisD}
111+
112+
113+
def test_gather_properties(registry):
114+
class_properties, method_properties = registry.gather_properties()
115+
116+
expected_class_properties = {
117+
"CustomNemesisA": {"flag_a": True, "flag_common": True},
118+
"CustomNemesisB": {"flag_b": True, "flag_common": True},
119+
"CustomNemesisC": {"flag_c": True},
120+
"CustomNemesisD": {"flag_a": True, "flag_d": True, "flag_common": True},
121+
}
122+
123+
expected_method_properties = {
124+
"disrupt_method_a": {"flag_a": True, "flag_common": True},
125+
"disrupt_method_b": {"flag_b": True, "flag_common": True},
126+
"disrupt_method_c": {"flag_c": True},
127+
"disrupt_method_d": {"flag_a": True, "flag_d": True, "flag_common": True},
128+
}
129+
130+
assert class_properties == expected_class_properties
131+
assert method_properties == expected_method_properties
132+
133+
134+
@pytest.mark.parametrize(
135+
"logical_phrase, expected_methods",
136+
[
137+
("flag_a", {CustomNemesisA.disrupt_method_a, CustomNemesisD.disrupt_method_d}),
138+
("flag_b", {CustomNemesisB.disrupt_method_b}),
139+
("flag_common and not flag_b", {CustomNemesisA.disrupt_method_a, CustomNemesisD.disrupt_method_d}),
140+
("flag_c", {CustomNemesisC.disrupt_method_c}),
141+
("flag_d", {CustomNemesisD.disrupt_method_d}),
142+
],
143+
)
144+
def test_get_disrupt_methods(registry, logical_phrase, expected_methods):
145+
disrupt_methods = registry.get_disrupt_methods(logical_phrase)
146+
assert set(disrupt_methods) == expected_methods
147+
148+
149+
def test_get_disrupt_method_execution(registry, capsys):
150+
"""Tests how you can use get_disrupt_methods to actually call returned methods"""
151+
disrupt_methods = registry.get_disrupt_methods("flag_common and not flag_b")
152+
for disrupt_method in disrupt_methods:
153+
disrupt_method(TestNemesis())
154+
155+
captured = capsys.readouterr()
156+
assert "called test function a" in captured.out
157+
assert "called test function d" in captured.out

unit_tests/nemesis/test_sisyphus.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
"""
2+
This module tests Nemesis/SisyphusMonkey specific class feature directly, with custom Subclass tree
3+
Should not be dependent on the implementation of Nemesis class
4+
5+
"""
6+
import pytest
7+
8+
from sdcm.nemesis import Nemesis, SisyphusMonkey
9+
from sdcm.nemesis_registry import NemesisRegistry
10+
from unit_tests.nemesis.fake_cluster import FakeTester, PARAMS, Cluster, Node
11+
from unit_tests.test_tester import ClusterTesterForTests
12+
13+
14+
class TestNemesisClass(Nemesis):
15+
COMMON_STRING = "called test function "
16+
kubernetes = False
17+
flag_a = False
18+
flag_b = False
19+
flag_c = False
20+
flag_common = False
21+
22+
def __init__(self, tester_obj, termination_event, *args, nemesis_selector=None, nemesis_seed=None, **kwargs):
23+
super().__init__(tester_obj, termination_event, *args, nemesis_selector=nemesis_selector,
24+
nemesis_seed=nemesis_seed, **kwargs)
25+
self.nemesis_registry = NemesisRegistry(base_class=TestNemesisClass)
26+
27+
def disrupt_method_a(self):
28+
print(self.COMMON_STRING + "a")
29+
30+
def disrupt_method_b(self):
31+
print(self.COMMON_STRING + "b")
32+
33+
def disrupt_method_c(self):
34+
print(self.COMMON_STRING + "c")
35+
36+
37+
class AddRemoveDCMonkey(TestNemesisClass):
38+
flag_common = True
39+
40+
@TestNemesisClass.add_disrupt_method
41+
def disrupt_rnd_method(self):
42+
print("disrupt_rnd_method")
43+
44+
def disrupt(self):
45+
self.disrupt_rnd_method()
46+
47+
48+
class DisabledMonkey(TestNemesisClass):
49+
disabled = True
50+
flag_b = True
51+
flag_common = True
52+
53+
def disrupt(self):
54+
self.disrupt_method_b()
55+
56+
57+
class DisruptAMonkey(TestNemesisClass):
58+
flag_a = True
59+
flag_common = True
60+
61+
def disrupt(self):
62+
self.disrupt_method_a()
63+
64+
65+
class DisruptCMonkey(TestNemesisClass):
66+
flag_c = True
67+
68+
def disrupt(self):
69+
self.disrupt_method_c()
70+
71+
72+
# Use multiple inheritance to ensure we overide registry after Nemesis but before Sisyphus
73+
class FakeSisyphusMonkey(SisyphusMonkey, TestNemesisClass):
74+
def __init__(self, tester_obj, *args, termination_event=None, nemesis_selector=None, nemesis_seed=None, **kwargs):
75+
super().__init__(tester_obj, termination_event, *args, nemesis_selector=nemesis_selector,
76+
nemesis_seed=nemesis_seed, **kwargs)
77+
78+
79+
@pytest.fixture()
80+
def get_sisyphus():
81+
def _create_sisyphus(params=PARAMS):
82+
return FakeSisyphusMonkey(FakeTester(params=params))
83+
return _create_sisyphus
84+
85+
86+
@pytest.mark.parametrize(
87+
"params, expected",
88+
[
89+
pytest.param({"nemesis_exclude_disabled": True},
90+
{"disrupt_method_a", "disrupt_method_c", "disrupt_rnd_method"},
91+
id="exclude_disabled"),
92+
pytest.param({"nemesis_exclude_disabled": False},
93+
{"disrupt_method_a", "disrupt_method_c", "disrupt_rnd_method", "disrupt_method_b"},
94+
id="disabled"),
95+
]
96+
)
97+
def test_disruptions_list(get_sisyphus, params, expected):
98+
if params:
99+
params.update(PARAMS)
100+
nemesis = get_sisyphus(params=params)
101+
assert expected == set(method.__name__ for method in nemesis.disruptions_list)
102+
103+
104+
def test_list_nemesis_of_added_disrupt_methods(get_sisyphus, capsys):
105+
nemesis = get_sisyphus()
106+
nemesis.disruptions_list = nemesis.build_disruptions_by_name(['disrupt_rnd_method'])
107+
nemesis.call_next_nemesis()
108+
captured = capsys.readouterr()
109+
assert "disrupt_rnd_method" in captured.out
110+
111+
112+
def test_add_sisyphus_with_filter_in_parallel_nemesis_run():
113+
tester = ClusterTesterForTests()
114+
tester._init_params()
115+
tester.db_cluster = Cluster(nodes=[Node(), Node()])
116+
tester.db_cluster.params = tester.params
117+
tester.params["nemesis_class_name"] = "SisyphusMonkey:1 SisyphusMonkey:2"
118+
tester.params["nemesis_selector"] = ["flag_common",
119+
"flag_common and not flag_a",
120+
"flag_c"]
121+
tester.params["nemesis_exclude_disabled"] = True
122+
tester.params["nemesis_multiply_factor"] = 1
123+
nemesises = tester.get_nemesis_class()
124+
125+
expected_selectors = ["flag_common", "flag_common and not flag_a", "flag_c"]
126+
for i, nemesis_settings in enumerate(nemesises):
127+
assert nemesis_settings['nemesis'] == SisyphusMonkey, \
128+
f"Wrong instance of nemesis class {nemesis_settings['nemesis']} expected SisyphusMonkey"
129+
assert nemesis_settings['nemesis_selector'] == expected_selectors[i], \
130+
f"Wrong nemesis filter selecters {nemesis_settings['nemesis_selector']} expected {expected_selectors[i]}"
131+
132+
active_nemesis = []
133+
for nemesis in nemesises:
134+
sisyphus = FakeSisyphusMonkey(tester, nemesis_selector=nemesis["nemesis_selector"])
135+
active_nemesis.append(sisyphus)
136+
137+
expected_methods = [{"disrupt_method_a", "disrupt_rnd_method"},
138+
{"disrupt_rnd_method"},
139+
{"disrupt_method_c"}]
140+
for i, nem in enumerate(active_nemesis):
141+
assert {disrupt.__name__ for disrupt in nem.disruptions_list} == expected_methods[i]

0 commit comments

Comments
 (0)