-
Notifications
You must be signed in to change notification settings - Fork 101
[Backport 2025.2] improvement(nemesis_tests): Make tests independant from production Nemesis #10957
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
vponomaryov
merged 2 commits into
scylladb:branch-2025.2
from
scylladbbot:backport/10912/to-2025.2
May 27, 2025
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
"""Mock classes""" | ||
|
||
from dataclasses import dataclass, field | ||
|
||
from sdcm.cluster import BaseScyllaCluster | ||
from unit_tests.dummy_remote import LocalLoaderSetDummy | ||
|
||
|
||
PARAMS = dict(nemesis_interval=1, nemesis_filter_seeds=False) | ||
|
||
|
||
@dataclass | ||
class Node: | ||
running_nemesis = None | ||
public_ip_address: str = '127.0.0.1' | ||
name: str = 'Node1' | ||
|
||
@property | ||
def scylla_shards(self): | ||
return 8 | ||
|
||
def log_message(self, *args, **kwargs): | ||
pass | ||
|
||
|
||
@dataclass | ||
class Cluster: | ||
nodes: list | ||
params: dict = field(default_factory=lambda: PARAMS) | ||
|
||
def check_cluster_health(self): | ||
pass | ||
|
||
@property | ||
def data_nodes(self): | ||
return self.nodes | ||
|
||
@property | ||
def zero_nodes(self): | ||
return self.nodes | ||
|
||
def log_message(self, *args, **kwargs): | ||
pass | ||
|
||
|
||
@dataclass | ||
class FakeTester: | ||
params: dict = field(default_factory=lambda: PARAMS) | ||
loaders: LocalLoaderSetDummy = field(default_factory=LocalLoaderSetDummy) | ||
db_cluster: Cluster | BaseScyllaCluster = field(default_factory=lambda: Cluster(nodes=[Node(), Node()])) | ||
monitors: list = field(default_factory=list) | ||
|
||
def __post_init__(self): | ||
self.db_cluster.params = self.params | ||
|
||
def create_stats(self): | ||
pass | ||
|
||
def update(self, *args, **kwargs): | ||
pass | ||
|
||
def get_scylla_versions(self): | ||
pass | ||
|
||
def get_test_details(self): | ||
pass | ||
|
||
def id(self): | ||
return 0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
""" | ||
This module tests NemesisRegistry method on custom Subclass tree | ||
Should not be dependent on the implementation of Nemesis class | ||
""" | ||
|
||
import pytest | ||
from sdcm.nemesis_registry import NemesisRegistry | ||
|
||
|
||
class TestNemesis: | ||
COMMON_STRING = "called test function " | ||
flag_a = False | ||
flag_b = False | ||
flag_c = False | ||
flag_d = False | ||
flag_common = False | ||
|
||
def disrupt_method_a(self): | ||
print(self.COMMON_STRING + "a") | ||
|
||
def disrupt_method_b(self): | ||
print(self.COMMON_STRING + "b") | ||
|
||
def disrupt_method_c(self): | ||
print(self.COMMON_STRING + "c") | ||
|
||
def disrupt_method_d(self): | ||
print(self.COMMON_STRING + "d") | ||
|
||
|
||
class CustomNemesisA(TestNemesis): | ||
flag_a = True | ||
flag_common = True | ||
|
||
def disrupt(self): | ||
self.disrupt_method_a() | ||
|
||
|
||
class CustomNemesisB(TestNemesis): | ||
flag_b = True | ||
flag_common = True | ||
|
||
def disrupt(self): | ||
self.disrupt_method_b() | ||
|
||
|
||
class CustomNemesisC(CustomNemesisA): | ||
flag_c = True | ||
|
||
def disrupt(self): | ||
self.disrupt_method_c() | ||
|
||
|
||
class CustomNemesisD(CustomNemesisB): | ||
flag_d = True | ||
flag_a = True | ||
flag_common = True | ||
|
||
def disrupt(self): | ||
self.disrupt_method_d() | ||
|
||
|
||
@pytest.fixture | ||
def registry(): | ||
return NemesisRegistry(base_class=TestNemesis) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"logical_phrase, expected_classes", | ||
[ | ||
("flag_a", {CustomNemesisA, CustomNemesisD}), | ||
("flag_b", {CustomNemesisB}), | ||
("flag_c", {CustomNemesisC}), | ||
("flag_d", {CustomNemesisD}), | ||
("flag_common", {CustomNemesisA, CustomNemesisB, CustomNemesisD}), | ||
], | ||
) | ||
def test_filter_subclasses_by_single_flag(registry, logical_phrase, expected_classes): | ||
filtered = registry.filter_subclasses(registry.get_subclasses(), logical_phrase) | ||
assert set(filtered) == expected_classes | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"logical_phrase, expected_classes", | ||
[ | ||
("flag_a and flag_d", {CustomNemesisD}), | ||
("flag_common and not flag_c", {CustomNemesisA, CustomNemesisB, CustomNemesisD}), | ||
("CustomNemesisA or flag_d", {CustomNemesisA, CustomNemesisD}), | ||
], | ||
) | ||
def test_filter_subclasses_by_combined_flags(registry, logical_phrase, expected_classes): | ||
filtered = registry.filter_subclasses(registry.get_subclasses(), logical_phrase) | ||
assert set(filtered) == expected_classes | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"logical_phrase, expected_classes", | ||
[ | ||
("(CustomNemesisA or CustomNemesisD) and not flag_d", {CustomNemesisA}), | ||
("flag_common and not flag_a", {CustomNemesisB}), | ||
], | ||
) | ||
def test_filter_subclasses_with_complex_expression(registry, logical_phrase, expected_classes): | ||
filtered = registry.filter_subclasses(registry.get_subclasses(), logical_phrase) | ||
assert set(filtered) == expected_classes | ||
|
||
|
||
def test_get_subclasses(registry): | ||
subclasses = registry.get_subclasses() | ||
assert set(subclasses) == {CustomNemesisA, CustomNemesisB, CustomNemesisC, CustomNemesisD} | ||
|
||
|
||
def test_gather_properties(registry): | ||
class_properties, method_properties = registry.gather_properties() | ||
|
||
expected_class_properties = { | ||
"CustomNemesisA": {"flag_a": True, "flag_common": True}, | ||
"CustomNemesisB": {"flag_b": True, "flag_common": True}, | ||
"CustomNemesisC": {"flag_c": True}, | ||
"CustomNemesisD": {"flag_a": True, "flag_d": True, "flag_common": True}, | ||
} | ||
|
||
expected_method_properties = { | ||
"disrupt_method_a": {"flag_a": True, "flag_common": True}, | ||
"disrupt_method_b": {"flag_b": True, "flag_common": True}, | ||
"disrupt_method_c": {"flag_c": True}, | ||
"disrupt_method_d": {"flag_a": True, "flag_d": True, "flag_common": True}, | ||
} | ||
|
||
assert class_properties == expected_class_properties | ||
assert method_properties == expected_method_properties | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"logical_phrase, expected_methods", | ||
[ | ||
("flag_a", {CustomNemesisA.disrupt_method_a, CustomNemesisD.disrupt_method_d}), | ||
("flag_b", {CustomNemesisB.disrupt_method_b}), | ||
("flag_common and not flag_b", {CustomNemesisA.disrupt_method_a, CustomNemesisD.disrupt_method_d}), | ||
("flag_c", {CustomNemesisC.disrupt_method_c}), | ||
("flag_d", {CustomNemesisD.disrupt_method_d}), | ||
], | ||
) | ||
def test_get_disrupt_methods(registry, logical_phrase, expected_methods): | ||
disrupt_methods = registry.get_disrupt_methods(logical_phrase) | ||
assert set(disrupt_methods) == expected_methods | ||
|
||
|
||
def test_get_disrupt_method_execution(registry, capsys): | ||
"""Tests how you can use get_disrupt_methods to actually call returned methods""" | ||
disrupt_methods = registry.get_disrupt_methods("flag_common and not flag_b") | ||
for disrupt_method in disrupt_methods: | ||
disrupt_method(TestNemesis()) | ||
|
||
captured = capsys.readouterr() | ||
assert "called test function a" in captured.out | ||
assert "called test function d" in captured.out |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
""" | ||
This module tests Nemesis/SisyphusMonkey specific class feature directly, with custom Subclass tree | ||
Should not be dependent on the implementation of Nemesis class | ||
""" | ||
import pytest | ||
|
||
from sdcm.nemesis import Nemesis, SisyphusMonkey | ||
from sdcm.nemesis_registry import NemesisRegistry | ||
from unit_tests.nemesis.fake_cluster import FakeTester, PARAMS, Cluster, Node | ||
from unit_tests.test_tester import ClusterTesterForTests | ||
|
||
|
||
class TestNemesisClass(Nemesis): | ||
COMMON_STRING = "called test function " | ||
kubernetes = False | ||
flag_a = False | ||
flag_b = False | ||
flag_c = False | ||
flag_common = False | ||
|
||
def __init__(self, tester_obj, termination_event, *args, nemesis_selector=None, nemesis_seed=None, **kwargs): | ||
super().__init__(tester_obj, termination_event, *args, nemesis_selector=nemesis_selector, | ||
nemesis_seed=nemesis_seed, **kwargs) | ||
self.nemesis_registry = NemesisRegistry(base_class=TestNemesisClass) | ||
|
||
def disrupt_method_a(self): | ||
print(self.COMMON_STRING + "a") | ||
|
||
def disrupt_method_b(self): | ||
print(self.COMMON_STRING + "b") | ||
|
||
def disrupt_method_c(self): | ||
print(self.COMMON_STRING + "c") | ||
|
||
|
||
class AddRemoveDCMonkey(TestNemesisClass): | ||
flag_common = True | ||
|
||
@TestNemesisClass.add_disrupt_method | ||
def disrupt_rnd_method(self): | ||
print("disrupt_rnd_method") | ||
|
||
def disrupt(self): | ||
self.disrupt_rnd_method() | ||
|
||
|
||
class DisabledMonkey(TestNemesisClass): | ||
disabled = True | ||
flag_b = True | ||
flag_common = True | ||
|
||
def disrupt(self): | ||
self.disrupt_method_b() | ||
|
||
|
||
class DisruptAMonkey(TestNemesisClass): | ||
flag_a = True | ||
flag_common = True | ||
|
||
def disrupt(self): | ||
self.disrupt_method_a() | ||
|
||
|
||
class DisruptCMonkey(TestNemesisClass): | ||
flag_c = True | ||
|
||
def disrupt(self): | ||
self.disrupt_method_c() | ||
|
||
|
||
# Use multiple inheritance to ensure we overide registry after Nemesis but before Sisyphus | ||
class FakeSisyphusMonkey(SisyphusMonkey, TestNemesisClass): | ||
def __init__(self, tester_obj, *args, termination_event=None, nemesis_selector=None, nemesis_seed=None, **kwargs): | ||
super().__init__(tester_obj, termination_event, *args, nemesis_selector=nemesis_selector, | ||
nemesis_seed=nemesis_seed, **kwargs) | ||
|
||
|
||
@pytest.fixture() | ||
def get_sisyphus(): | ||
def _create_sisyphus(params=PARAMS): | ||
return FakeSisyphusMonkey(FakeTester(params=params)) | ||
return _create_sisyphus | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"params, expected", | ||
[ | ||
pytest.param({"nemesis_exclude_disabled": True}, | ||
{"disrupt_method_a", "disrupt_method_c", "disrupt_rnd_method"}, | ||
id="exclude_disabled"), | ||
pytest.param({"nemesis_exclude_disabled": False}, | ||
{"disrupt_method_a", "disrupt_method_c", "disrupt_rnd_method", "disrupt_method_b"}, | ||
id="disabled"), | ||
] | ||
) | ||
def test_disruptions_list(get_sisyphus, params, expected): | ||
if params: | ||
params.update(PARAMS) | ||
nemesis = get_sisyphus(params=params) | ||
assert expected == set(method.__name__ for method in nemesis.disruptions_list) | ||
|
||
|
||
def test_list_nemesis_of_added_disrupt_methods(get_sisyphus, capsys): | ||
nemesis = get_sisyphus() | ||
nemesis.disruptions_list = nemesis.build_disruptions_by_name(['disrupt_rnd_method']) | ||
nemesis.call_next_nemesis() | ||
captured = capsys.readouterr() | ||
assert "disrupt_rnd_method" in captured.out | ||
|
||
|
||
def test_add_sisyphus_with_filter_in_parallel_nemesis_run(): | ||
tester = ClusterTesterForTests() | ||
tester._init_params() | ||
tester.db_cluster = Cluster(nodes=[Node(), Node()]) | ||
tester.db_cluster.params = tester.params | ||
tester.params["nemesis_class_name"] = "SisyphusMonkey:1 SisyphusMonkey:2" | ||
tester.params["nemesis_selector"] = ["flag_common", | ||
"flag_common and not flag_a", | ||
"flag_c"] | ||
tester.params["nemesis_exclude_disabled"] = True | ||
tester.params["nemesis_multiply_factor"] = 1 | ||
nemesises = tester.get_nemesis_class() | ||
|
||
expected_selectors = ["flag_common", "flag_common and not flag_a", "flag_c"] | ||
for i, nemesis_settings in enumerate(nemesises): | ||
assert nemesis_settings['nemesis'] == SisyphusMonkey, \ | ||
f"Wrong instance of nemesis class {nemesis_settings['nemesis']} expected SisyphusMonkey" | ||
assert nemesis_settings['nemesis_selector'] == expected_selectors[i], \ | ||
f"Wrong nemesis filter selecters {nemesis_settings['nemesis_selector']} expected {expected_selectors[i]}" | ||
|
||
active_nemesis = [] | ||
for nemesis in nemesises: | ||
sisyphus = FakeSisyphusMonkey(tester, nemesis_selector=nemesis["nemesis_selector"]) | ||
active_nemesis.append(sisyphus) | ||
|
||
expected_methods = [{"disrupt_method_a", "disrupt_rnd_method"}, | ||
{"disrupt_rnd_method"}, | ||
{"disrupt_method_c"}] | ||
for i, nem in enumerate(active_nemesis): | ||
assert {disrupt.__name__ for disrupt in nem.disruptions_list} == expected_methods[i] |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This naming makes pytest think it is test class and produce following messages: