Skip to content

Commit

Permalink
Issue 118: Function for deactivating a mode (#119)
Browse files Browse the repository at this point in the history
Provides a function deactivate_method for deactivating a mode.

Also, rename:
   activate -> activate_one_of_multiple
   activate_using -> activate_method (paired with the deactivate_method)

Closes: #118
  • Loading branch information
fohrloop committed Dec 25, 2023
1 parent 1acf15d commit 4cd2007
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 34 deletions.
114 changes: 89 additions & 25 deletions tests/unit/test_core/test_activation/test_activation.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,31 +23,32 @@
from wakepy.core.activation import (
StageName,
UsageStatus,
activate,
activate_using,
activate_one_of_multiple,
activate_method,
caniuse_fails,
deactivate_method,
get_platform_supported,
should_fake_success,
try_enter_and_heartbeat,
)
from wakepy.core.calls import CallProcessor
from wakepy.core.heartbeat import Heartbeat
from wakepy.core.method import Method, PlatformName, get_methods
from wakepy.core.method import Method, MethodError, PlatformName, get_methods


def test_activate_without_methods(monkeypatch):
_arrange_for_test_activate(monkeypatch)
res, active_method, heartbeat = activate([], None)
res, active_method, heartbeat = activate_one_of_multiple([], None)
assert res.get_details() == []
assert res.success is False
assert active_method is None
assert heartbeat is None


def test_activate_function_success(monkeypatch):
"""Here we test the activate() function. It calls some other functions
which we do not care about as they're tested elsewhere. That is we why
monkeypatch those functions with fakes"""
"""Here we test the activate_one_of_multiple() function. It calls some
other functions which we do not care about as they're tested elsewhere.
That is we why monkeypatch those functions with fakes"""

# Arrange
mocks = _arrange_for_test_activate(monkeypatch)
Expand All @@ -58,7 +59,7 @@ def test_activate_function_success(monkeypatch):
# Note: prioritize the failing first, so that the failing one will also be
# used. This also tests at that the prioritization is used at least
# somehow
result, active_method, heartbeat = activate(
result, active_method, heartbeat = activate_one_of_multiple(
[methodcls_success, methodcls_fail],
call_processor=mocks.call_processor,
methods_priority=[
Expand Down Expand Up @@ -89,7 +90,7 @@ def test_activate_function_failure(monkeypatch):
methodcls_fail = get_test_method_class(enter_mode=False)

# Act
result, active_method, heartbeat = activate(
result, active_method, heartbeat = activate_one_of_multiple(
[methodcls_fail],
call_processor=mocks.call_processor,
)
Expand All @@ -102,12 +103,14 @@ def test_activate_function_failure(monkeypatch):


def _arrange_for_test_activate(monkeypatch):
"""This is the test arrangement step for tests for the `activate` function"""
"""This is the test arrangement step for tests for the
`activate_one_of_multiple` function"""

mocks = Mock()
mocks.heartbeat = Mock(spec_set=Heartbeat)
mocks.call_processor = Mock(spec_set=CallProcessor)

def fake_activate_using(method):
def fake_activate_method(method):
success = method.enter_mode()
return (
MethodActivationResult(
Expand All @@ -118,15 +121,15 @@ def fake_activate_using(method):
mocks.heartbeat,
)

monkeypatch.setattr("wakepy.core.activation.activate_using", fake_activate_using)
monkeypatch.setattr("wakepy.core.activation.activate_method", fake_activate_method)
monkeypatch.setattr(
"wakepy.core.activation.check_methods_priority", mocks.check_methods_priority
)
monkeypatch.setenv("WAKEPY_FAKE_SUCCESS", "0")
return mocks


def test_activate_using_method_without_name():
def test_activate_method_method_without_name():
"""Methods used for activation must have a name. If not, there should be
a ValueError raised"""

Expand All @@ -135,10 +138,10 @@ def test_activate_using_method_without_name():
ValueError,
match=re.escape("Methods without a name may not be used to activate modes!"),
):
activate_using(method)
activate_method(method)


def test_activate_using_method_without_platform_support(monkeypatch):
def test_activate_method_method_without_platform_support(monkeypatch):
WindowsMethod = get_test_method_class(
supported_platforms=(PlatformName.WINDOWS,),
)
Expand All @@ -148,16 +151,16 @@ def test_activate_using_method_without_platform_support(monkeypatch):

# The current platform is set to linux, so method supporting only linux
# should fail.
res, heartbeat = activate_using(winmethod)
res, heartbeat = activate_method(winmethod)
assert res.failure_stage == StageName.PLATFORM_SUPPORT
assert res.status == UsageStatus.FAIL
assert heartbeat is None


def test_activate_using_method_caniuse_fails():
def test_activate_method_method_caniuse_fails():
# Case 1: Fail by returning False from caniuse
method = get_test_method_class(caniuse=False, enter_mode=True, exit_mode=True)()
res, heartbeat = activate_using(method)
res, heartbeat = activate_method(method)
assert res.status == UsageStatus.FAIL
assert res.failure_stage == StageName.REQUIREMENTS
assert res.message == ""
Expand All @@ -167,36 +170,36 @@ def test_activate_using_method_caniuse_fails():
method = get_test_method_class(
caniuse="SomeSW version <2.1.5 not supported", enter_mode=True, exit_mode=True
)()
res, heartbeat = activate_using(method)
res, heartbeat = activate_method(method)
assert res.status == UsageStatus.FAIL
assert res.failure_stage == StageName.REQUIREMENTS
assert res.message == "SomeSW version <2.1.5 not supported"
assert heartbeat is None


def test_activate_using_method_enter_mode_fails():
def test_activate_method_method_enter_mode_fails():
# Case: Fail by returning False from enter_mode
method = get_test_method_class(caniuse=True, enter_mode=False)()
res, heartbeat = activate_using(method)
res, heartbeat = activate_method(method)
assert res.status == UsageStatus.FAIL
assert res.failure_stage == StageName.ACTIVATION
assert res.message == ""
assert heartbeat is None


def test_activate_using_enter_mode_success():
def test_activate_method_enter_mode_success():
method = get_test_method_class(caniuse=True, enter_mode=True)()
res, heartbeat = activate_using(method)
res, heartbeat = activate_method(method)
assert res.status == UsageStatus.SUCCESS
assert res.failure_stage is None
assert res.message == ""
# No heartbeat on success, as the used Method does not have heartbeat()
assert heartbeat is None


def test_activate_using_heartbeat_success():
def test_activate_method_heartbeat_success():
method = get_test_method_class(heartbeat=True)()
res, heartbeat = activate_using(method)
res, heartbeat = activate_method(method)
assert res.status == UsageStatus.SUCCESS
assert res.failure_stage is None
assert res.message == ""
Expand Down Expand Up @@ -525,6 +528,67 @@ def test_method_usage_result(
assert str(mur) == expected_string_representation


def test_deactivate_success_no_heartbeat():
method = get_test_method_class(enter_mode=True, exit_mode=True)()
deactivate_method(method)


def test_deactivate_success_with_heartbeat():
heartbeat = Mock(spec_set=Heartbeat)
heartbeat.stop.return_value = True
method = get_test_method_class(enter_mode=True, exit_mode=True)()
deactivate_method(method, heartbeat=heartbeat)


def test_deactivate_success_with_heartbeat_and_no_exit():
heartbeat = Mock(spec_set=Heartbeat)
heartbeat.stop.return_value = True
method = get_test_method_class(enter_mode=True)()
deactivate_method(method, heartbeat=heartbeat)


def test_deactivate_fail_exit_mode_returning_bad_value():
method = get_test_method_class(enter_mode=True, exit_mode=123)()
with pytest.raises(
MethodError,
match=re.escape(
f"The exit_mode of {method.__class__.__name__} ({method.name}) returned a "
"value of unsupported type. The supported types are: bool, str. "
"Returned value: 123"
),
):
deactivate_method(method)


def test_deactivate_fail_exit_mode_returning_string():
method = get_test_method_class(enter_mode=True, exit_mode="oh no")()
with pytest.raises(
MethodError,
match=re.escape(
f"The exit_mode of '{method.__class__.__name__}' ({method.name}) was "
"unsuccessful"
)
+ ".*"
+ re.escape("Returned value: oh no"),
):
deactivate_method(method)


def test_deactivate_fail_heartbeat_not_stopping():
heartbeat = Mock(spec_set=Heartbeat)
heartbeat.stop.return_value = "Bad value"
method = get_test_method_class(enter_mode=True, exit_mode=True)()
with pytest.raises(
MethodError,
match=re.escape(
f"The heartbeat of {method.__class__.__name__} ({method.name}) could not "
"be stopped! Suggesting submitting a bug report and rebooting for clearing "
"the mode."
),
):
deactivate_method(method, heartbeat)


def test_stagename():
assert StageName.PLATFORM_SUPPORT == "PLATFORM_SUPPORT"
assert StageName.ACTIVATION == "ACTIVATION"
Expand Down
47 changes: 42 additions & 5 deletions wakepy/core/activation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Most important functions
------------------------
activate_using(method:Method) -> MethodActivationResult
activate_method(method:Method) -> MethodActivationResult
Activate a mode using a single Method
get_prioritized_methods
Prioritize of collection of Methods
Expand Down Expand Up @@ -243,7 +243,7 @@ def __repr__(self):
return f"({self.status}{error_at}, {self.method_name}{message_part})"


def activate(
def activate_one_of_multiple(
methods: list[Type[Method]],
call_processor: CallProcessor,
methods_priority: Optional[MethodsPriorityOrder] = None,
Expand Down Expand Up @@ -277,7 +277,7 @@ def activate(

for methodcls in prioritized_methods:
method = methodcls(call_processor=call_processor)
methodresult, heartbeat = activate_using(method)
methodresult, heartbeat = activate_method(method)
results.append(methodresult)
if methodresult.status == UsageStatus.SUCCESS:
break
Expand Down Expand Up @@ -508,8 +508,8 @@ def get_prioritized_methods(
return [method for group in ordered_groups for method in group]


def activate_using(method: Method) -> Tuple[MethodActivationResult, Heartbeat | None]:
"""Activates a mode defined by a Method.
def activate_method(method: Method) -> Tuple[MethodActivationResult, Heartbeat | None]:
"""Activates a mode defined by a single Method.
Returns
-------
Expand Down Expand Up @@ -552,6 +552,43 @@ def activate_using(method: Method) -> Tuple[MethodActivationResult, Heartbeat |
return result, heartbeat


def deactivate_method(method: Method, heartbeat: Optional[Heartbeat] = None) -> None:
"""Deactivates a mode defined by the `method`.
Raises
------
MethodError (RuntimeError), if the deactivation was not successful.
"""

heartbeat_stopped = heartbeat.stop() if heartbeat is not None else True

if method.has_exit:
retval = method.exit_mode()
if not isinstance(retval, (bool, str)):
raise MethodError(
f"The exit_mode of {method.__class__.__name__} ({method.name}) "
"returned a value of unsupported type. The supported types are: "
f"bool, str. Returned value: {retval}"
)
if isinstance(retval, str) or retval is False:
raise MethodError(
f"The exit_mode of '{method.__class__.__name__}' ({method.name}) was "
"unsuccessful! This should never happen, and could mean that the "
"implementation has a bug. Entering the mode has been successful, and "
"since exiting was not, your system might stil be in the mode defined "
f"by the '{method.__class__.__name__}', or not. Suggesting submitting "
f"a bug report and rebooting for clearing the mode. "
f"Returned value: {retval}"
)

if heartbeat_stopped is not True:
raise MethodError(
f"The heartbeat of {method.__class__.__name__} ({method.name}) could not "
"be stopped! Suggesting submitting a bug report and rebooting for "
"clearing the mode. "
)


def get_platform_supported(method: Method, platform: PlatformName) -> bool:
"""Checks if method is supported by the platform
Expand Down
7 changes: 5 additions & 2 deletions wakepy/core/heartbeat.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,8 @@ def __init__(
self.method = method
self.prev_call = heartbeat_call_time

def start(self):
...
def start(self) -> bool:
return True

def stop(self) -> bool:
return True
4 changes: 2 additions & 2 deletions wakepy/core/mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import typing
from abc import ABC

from .activation import ActivationResult, activate
from .activation import ActivationResult, activate_one_of_multiple
from .calls import CallProcessor
from .heartbeat import Heartbeat
from .method import get_methods_for_mode, select_methods
Expand Down Expand Up @@ -49,7 +49,7 @@ def activate(
method_classes: list[Type[Method]],
methods_priority: Optional[MethodsPriorityOrder] = None,
) -> ActivationResult:
result, active_method, heartbeat = activate(
result, active_method, heartbeat = activate_one_of_multiple(
methods=method_classes,
methods_priority=methods_priority,
call_processor=self.call_processor,
Expand Down

0 comments on commit 4cd2007

Please sign in to comment.