Skip to content

Commit 6a95f9f

Browse files
committed
tests: Improving OBS tests, adding tests for analytics and multi-step
1 parent 3852640 commit 6a95f9f

File tree

6 files changed

+319
-8
lines changed

6 files changed

+319
-8
lines changed

dk-installer.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -750,7 +750,7 @@ def on_action_fail(self, action, args):
750750
pass
751751

752752
def __str__(self):
753-
return self.label or self.__name__
753+
return self.label or self.__class__.__name__
754754

755755

756756
class MultiStepAction(Action):
@@ -783,11 +783,9 @@ def execute(self, args):
783783

784784
self._print_intro_text(args)
785785
CONSOLE.space()
786-
executed_steps: list[Step] = []
787786
action_fail_exception = None
788787
action_fail_step = None
789788
for step in action_steps:
790-
executed_steps.append(step)
791789
with CONSOLE.partial() as partial:
792790
partial(f"{step.label}... ")
793791
try:
@@ -813,7 +811,7 @@ def execute(self, args):
813811
else:
814812
CONSOLE.title(f"{self.label} SUCCEEDED")
815813

816-
for step in reversed(executed_steps):
814+
for step in reversed(action_steps):
817815
try:
818816
if action_fail_exception is None:
819817
LOG.debug("Running [%s] on-action-success", step)

tests/conftest.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,15 @@ def tmp_data_folder(action_cls):
9999
yield data_folder
100100

101101

102+
@pytest.fixture
103+
def tmp_logs_folder(action_cls):
104+
with (
105+
TemporaryDirectory() as data_folder,
106+
patch.object(action_cls, "logs_folder", new=Path(data_folder), create=True),
107+
):
108+
yield data_folder
109+
110+
102111
@pytest.fixture
103112
def demo_config_path(tmp_data_folder):
104113
path = Path(tmp_data_folder).joinpath("demo-config.json")

tests/test_analytics_wrapper.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
from pathlib import Path
2+
from ssl import SSLContext
3+
4+
import pytest
5+
from unittest.mock import patch, ANY
6+
from .installer import AnalyticsWrapper
7+
8+
9+
@pytest.fixture
10+
def urlopen_mock():
11+
with patch("urllib.request.urlopen") as mock:
12+
yield mock
13+
14+
15+
@pytest.fixture
16+
def analytics_wrapper(action, args_mock):
17+
yield AnalyticsWrapper(action, args_mock)
18+
19+
20+
@pytest.fixture
21+
def instance_id_mock(analytics_wrapper):
22+
with patch.object(analytics_wrapper, "get_instance_id", return_value="test-instance-id") as mock:
23+
yield mock
24+
25+
26+
@pytest.mark.integration
27+
def test_send_on_exit(analytics_wrapper, action, urlopen_mock, instance_id_mock):
28+
with analytics_wrapper:
29+
urlopen_mock.assert_not_called()
30+
assert hasattr(action, "analytics")
31+
32+
urlopen_mock.assert_called_once()
33+
req_call = urlopen_mock.call_args_list[0]
34+
assert req_call.args[0].full_url == "https://api.mixpanel.com/track?ip=1"
35+
assert req_call.args[0].method == "POST"
36+
assert isinstance(req_call.kwargs["context"], SSLContext)
37+
assert req_call.kwargs["timeout"] == 3
38+
39+
40+
@pytest.mark.integration
41+
def test_exception_handling(analytics_wrapper, action, urlopen_mock, instance_id_mock):
42+
urlopen_mock.side_effect = RuntimeError
43+
with analytics_wrapper:
44+
pass
45+
46+
urlopen_mock.assert_called_once()
47+
48+
49+
@pytest.mark.integration
50+
def test_event_data(analytics_wrapper, action, urlopen_mock, instance_id_mock):
51+
with patch.object(analytics_wrapper, "send_mp_request") as mp_req_mock:
52+
with analytics_wrapper:
53+
analytics_wrapper.additional_properties["ap"] = "additional"
54+
55+
mp_req_mock.assert_called_once_with(
56+
"track?ip=1",
57+
{
58+
"event": "test_prod-test",
59+
"properties": {
60+
"token": ANY,
61+
"prod": "test_prod",
62+
"action": "test",
63+
"ap": "additional",
64+
"elapsed": ANY,
65+
"os_version": ANY,
66+
"os_arch": ANY,
67+
"$os": ANY,
68+
"python_info": ANY,
69+
"installer_version": ANY,
70+
"distinct_id": ANY,
71+
"instance_id": ANY,
72+
},
73+
},
74+
)
75+
76+
77+
@pytest.mark.integration
78+
def test_get_instance_id(analytics_wrapper, tmp_logs_folder):
79+
Path(tmp_logs_folder, "instance.txt").write_text("some-instance-id")
80+
instance_id = analytics_wrapper.get_instance_id()
81+
assert instance_id == "some-instance-id"
82+
83+
84+
@pytest.mark.integration
85+
def test_get_instance_id_create(analytics_wrapper, tmp_logs_folder):
86+
instance_id = analytics_wrapper.get_instance_id()
87+
assert len(instance_id) == 16
88+
assert Path(tmp_logs_folder, "instance.txt").exists()
89+
90+
91+
@pytest.mark.unit
92+
@pytest.mark.parametrize(
93+
"instance_id,expected_hash", (("inst-1", "5f0c3843f9c1f38d"), ("inst-other", "1f59818ab9e8ce51"))
94+
)
95+
def test_hash_is_unique_per_instance(instance_id, expected_hash, analytics_wrapper, instance_id_mock):
96+
instance_id_mock.return_value = instance_id
97+
98+
assert analytics_wrapper._hash_value("anything") == expected_hash
99+
assert analytics_wrapper._hash_value(b"anything") == expected_hash

tests/test_multistep_action.py

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
from itertools import count
2+
from unittest.mock import Mock, call, ANY
3+
4+
import pytest
5+
6+
from tests.installer import MultiStepAction, Step, AbortAction, SkipStep, InstallerError, CommandFailed
7+
8+
9+
@pytest.fixture
10+
def step_mock():
11+
yield Mock()
12+
13+
14+
@pytest.fixture
15+
def step_factory(step_mock):
16+
idx = count()
17+
18+
def _create_mock(**kwargs):
19+
attrs = {}
20+
step_idx = next(idx)
21+
for step_attr in ("pre_execute", "execute", "on_action_success", "on_action_fail"):
22+
mock = Mock(side_effect=kwargs.pop(step_attr, None))
23+
step_mock.attach_mock(mock, f"step_{step_idx}_{step_attr}")
24+
attrs[step_attr] = mock
25+
26+
attrs.update(kwargs)
27+
28+
return type(f"TestStep{step_idx}", (Step,), attrs)
29+
30+
return _create_mock
31+
32+
33+
@pytest.mark.unit
34+
@pytest.mark.parametrize(
35+
"step_1_args",
36+
({}, {"execute": ValueError, "required": False}, {"execute": SkipStep}),
37+
ids=("regular execution", "step skipped", "non required step fails"),
38+
)
39+
def test_execute(step_1_args, step_mock, step_factory, args_mock):
40+
class TestMSAction(MultiStepAction):
41+
steps = (
42+
step_factory(),
43+
step_factory(**step_1_args),
44+
step_factory(),
45+
)
46+
47+
action = TestMSAction()
48+
action.execute(args_mock)
49+
50+
step_mock.assert_has_calls(
51+
[
52+
call.step_0_pre_execute(ANY, args_mock),
53+
call.step_1_pre_execute(ANY, args_mock),
54+
call.step_2_pre_execute(ANY, args_mock),
55+
call.step_0_execute(ANY, args_mock),
56+
call.step_1_execute(ANY, args_mock),
57+
call.step_2_execute(ANY, args_mock),
58+
call.step_2_on_action_success(ANY, args_mock),
59+
call.step_1_on_action_success(ANY, args_mock),
60+
call.step_0_on_action_success(ANY, args_mock),
61+
]
62+
)
63+
64+
65+
@pytest.mark.unit
66+
def test_execute_not_required(step_mock, step_factory, args_mock):
67+
class TestMSAction(MultiStepAction):
68+
steps = (
69+
step_factory(),
70+
step_factory(),
71+
step_factory(),
72+
)
73+
74+
action = TestMSAction()
75+
action.execute(args_mock)
76+
77+
step_mock.assert_has_calls(
78+
[
79+
call.step_0_pre_execute(ANY, args_mock),
80+
call.step_1_pre_execute(ANY, args_mock),
81+
call.step_2_pre_execute(ANY, args_mock),
82+
call.step_0_execute(ANY, args_mock),
83+
call.step_1_execute(ANY, args_mock),
84+
call.step_2_execute(ANY, args_mock),
85+
call.step_2_on_action_success(ANY, args_mock),
86+
call.step_1_on_action_success(ANY, args_mock),
87+
call.step_0_on_action_success(ANY, args_mock),
88+
]
89+
)
90+
91+
92+
@pytest.mark.unit
93+
def test_execute_post_hook_fail(step_mock, step_factory, args_mock):
94+
class TestMSAction(MultiStepAction):
95+
steps = (
96+
step_factory(on_action_success=RuntimeError),
97+
step_factory(execute=SkipStep, on_action_success=ValueError),
98+
step_factory(on_action_success=InstallerError),
99+
)
100+
101+
action = TestMSAction()
102+
action.execute(args_mock)
103+
104+
step_mock.assert_has_calls(
105+
[
106+
call.step_0_pre_execute(ANY, args_mock),
107+
call.step_1_pre_execute(ANY, args_mock),
108+
call.step_2_pre_execute(ANY, args_mock),
109+
call.step_0_execute(ANY, args_mock),
110+
call.step_1_execute(ANY, args_mock),
111+
call.step_2_execute(ANY, args_mock),
112+
call.step_2_on_action_success(ANY, args_mock),
113+
call.step_1_on_action_success(ANY, args_mock),
114+
call.step_0_on_action_success(ANY, args_mock),
115+
]
116+
)
117+
118+
119+
@pytest.mark.unit
120+
@pytest.mark.parametrize("exc_class", (AbortAction, InstallerError, ValueError, CommandFailed))
121+
def test_execute_fail(exc_class, step_mock, step_factory, args_mock):
122+
abort_exc = exc_class()
123+
124+
class TestMSAction(MultiStepAction):
125+
steps = (
126+
step_factory(),
127+
step_factory(execute=abort_exc),
128+
step_factory(on_action_fail=RuntimeError),
129+
)
130+
131+
action = TestMSAction()
132+
133+
with pytest.raises(AbortAction if exc_class == AbortAction else InstallerError) as exc_info:
134+
action.execute(args_mock)
135+
136+
step_mock.assert_has_calls(
137+
[
138+
call.step_0_pre_execute(ANY, args_mock),
139+
call.step_1_pre_execute(ANY, args_mock),
140+
call.step_2_pre_execute(ANY, args_mock),
141+
call.step_0_execute(ANY, args_mock),
142+
call.step_1_execute(ANY, args_mock),
143+
call.step_2_on_action_fail(ANY, args_mock),
144+
call.step_1_on_action_fail(ANY, args_mock),
145+
call.step_0_on_action_fail(ANY, args_mock),
146+
]
147+
)
148+
149+
assert exc_info.value.__cause__ == abort_exc
150+
151+
152+
@pytest.mark.unit
153+
@pytest.mark.parametrize("exc_class", (AbortAction, InstallerError, ValueError, CommandFailed))
154+
def test_pre_execute_fail(exc_class, step_mock, step_factory, args_mock):
155+
abort_exc = exc_class()
156+
157+
class TestMSAction(MultiStepAction):
158+
steps = (
159+
step_factory(),
160+
step_factory(execute=abort_exc),
161+
step_factory(),
162+
)
163+
164+
action = TestMSAction()
165+
166+
with pytest.raises(AbortAction if exc_class == AbortAction else InstallerError) as exc_info:
167+
action.execute(args_mock)
168+
169+
step_mock.assert_has_calls(
170+
[
171+
call.step_0_pre_execute(ANY, args_mock),
172+
call.step_1_pre_execute(ANY, args_mock),
173+
]
174+
)
175+
176+
assert exc_info.value.__cause__ == abort_exc

tests/test_obs_expose.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import pytest
66

7-
from tests.installer import ObsExposeAction
7+
from tests.installer import ObsExposeAction, CommandFailed, AbortAction
88

99

1010
@pytest.fixture
@@ -55,3 +55,11 @@ def test_obs_expose(obs_expose_action, start_cmd_mock, stdout_mock, proc_mock, d
5555
],
5656
any_order=True,
5757
)
58+
59+
60+
@pytest.mark.integration
61+
def test_obs_expose_abort(obs_expose_action, start_cmd_mock):
62+
start_cmd_mock.__exit__.side_effect = CommandFailed
63+
64+
with pytest.raises(AbortAction):
65+
obs_expose_action.execute()

tests/test_obs_install.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import pytest
66

7-
from tests.installer import ObsInstallAction
7+
from tests.installer import ObsInstallAction, MinikubeProfileStep, AbortAction
88

99

1010
@pytest.fixture
@@ -17,11 +17,23 @@ def obs_install_action(action_cls, args_mock, tmp_data_folder, start_cmd_mock):
1717

1818

1919
@pytest.mark.integration
20-
def test_obs_install(obs_install_action, start_cmd_mock, tmp_data_folder):
20+
def test_obs_install(obs_install_action, start_cmd_mock, tmp_data_folder, stdout_mock):
21+
stdout_mock.side_effect = (
22+
[b"{}"],
23+
[],
24+
[],
25+
[],
26+
[],
27+
[],
28+
[],
29+
[b'{"service_account_key": "demo-account-key", "project_id": "test-project-id"}'],
30+
[],
31+
[],
32+
)
33+
2134
obs_install_action.execute()
2235

2336
def_call = partial(call, raise_on_non_zero=True, env=None)
24-
2537
start_cmd_mock.assert_has_calls(
2638
[
2739
def_call("minikube", "-p", "dk-observability", "status", "-o", "json", raise_on_non_zero=False),
@@ -92,3 +104,12 @@ def test_obs_install(obs_install_action, start_cmd_mock, tmp_data_folder):
92104
)
93105

94106
assert Path(tmp_data_folder).joinpath("dk-obs-credentials.txt").stat().st_size > 0
107+
assert Path(tmp_data_folder).joinpath("demo-config.json").stat().st_size > 0
108+
109+
110+
@pytest.mark.integration
111+
def test_obs_existing_install_abort(obs_install_action, stdout_mock):
112+
stdout_mock.side_effect = [[b'{"Name":"dk-observability","Host":"Running","Kubelet":"Running"}']]
113+
with patch.object(obs_install_action, "steps", new=[MinikubeProfileStep]):
114+
with pytest.raises(AbortAction):
115+
obs_install_action.execute()

0 commit comments

Comments
 (0)