test SafeEyesCore: initial
This commit is contained in:
parent
1d74ee2fbe
commit
affd1af5e0
|
@ -0,0 +1,345 @@
|
|||
from collections import deque
|
||||
import datetime
|
||||
import logging
|
||||
import pytest
|
||||
|
||||
from safeeyes import core
|
||||
from safeeyes import model
|
||||
|
||||
import threading
|
||||
from time import sleep
|
||||
|
||||
from unittest import mock
|
||||
|
||||
class TestSafeEyesCore:
|
||||
@pytest.fixture(autouse=True)
|
||||
def set_time(self, time_machine):
|
||||
time_machine.move_to(datetime.datetime.fromisoformat("2024-08-25T13:00"))
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def monkeypatch_translations(self, monkeypatch):
|
||||
monkeypatch.setattr(core, "_", lambda message: "translated!: " + message, raising=False)
|
||||
monkeypatch.setattr(model, "_", lambda message: "translated!: " + message, raising=False)
|
||||
|
||||
@pytest.fixture
|
||||
def sequential_threading(self, monkeypatch):
|
||||
# executes instantly
|
||||
# TODO: separate thread?
|
||||
monkeypatch.setattr(
|
||||
core.utility,
|
||||
"execute_main_thread",
|
||||
lambda target_function, *args, **kwargs: target_function(*args, **kwargs)
|
||||
)
|
||||
|
||||
|
||||
class Handle:
|
||||
thread = None
|
||||
task_queue = deque()
|
||||
running = True
|
||||
condvar_in = threading.Condition()
|
||||
condvar_out = threading.Condition()
|
||||
|
||||
def background_thread(self):
|
||||
while True:
|
||||
with self.condvar_in:
|
||||
success = self.condvar_in.wait(1)
|
||||
if not success:
|
||||
raise Exception("thread timed out")
|
||||
|
||||
if not self.running:
|
||||
logging.debug(f"background task shutdown")
|
||||
break
|
||||
|
||||
logging.debug(f"background task woken up")
|
||||
|
||||
if self.task_queue:
|
||||
(target_function, kwargs) = self.task_queue.popleft()
|
||||
logging.debug(f"thread started {target_function}")
|
||||
target_function(**kwargs)
|
||||
logging.debug(f"thread finished {target_function}")
|
||||
|
||||
with self.condvar_out:
|
||||
self.condvar_out.notify()
|
||||
|
||||
def sleep(self, time):
|
||||
if self.thread is threading.current_thread():
|
||||
with self.condvar_out:
|
||||
self.condvar_out.notify()
|
||||
with self.condvar_in:
|
||||
success = self.condvar_in.wait(1)
|
||||
if not success:
|
||||
raise Exception("thread timed out")
|
||||
|
||||
def utility_start_thread(self, target_function, **kwargs):
|
||||
self.task_queue.append((target_function, kwargs))
|
||||
|
||||
if self.thread is None:
|
||||
self.thread = threading.Thread(target=self.background_thread, name="WorkThread", daemon=False, kwargs=kwargs)
|
||||
self.thread.start()
|
||||
|
||||
def next(self):
|
||||
assert self.thread
|
||||
|
||||
with self.condvar_in:
|
||||
self.condvar_in.notify()
|
||||
|
||||
def wait(self):
|
||||
# wait until done:
|
||||
with self.condvar_out:
|
||||
success = self.condvar_out.wait(1)
|
||||
if not success:
|
||||
raise Exception("thread timed out")
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
with self.condvar_in:
|
||||
self.condvar_in.notify()
|
||||
|
||||
if self.thread:
|
||||
self.thread.join(1)
|
||||
|
||||
handle = Handle()
|
||||
|
||||
monkeypatch.setattr(
|
||||
core.utility,
|
||||
"start_thread",
|
||||
handle.utility_start_thread
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
core.time,
|
||||
"sleep",
|
||||
lambda time: handle.sleep(time)
|
||||
)
|
||||
|
||||
yield handle
|
||||
|
||||
handle.stop()
|
||||
|
||||
def test_create_empty(self):
|
||||
context = {}
|
||||
config = {
|
||||
"short_breaks": [],
|
||||
"long_breaks": [],
|
||||
"short_break_interval": 15,
|
||||
"long_break_interval": 75,
|
||||
"long_break_duration": 60,
|
||||
"short_break_duration": 15,
|
||||
"random_order": False,
|
||||
"postpone_duration": 5,
|
||||
}
|
||||
safe_eyes_core = core.SafeEyesCore(context)
|
||||
safe_eyes_core.initialize(config)
|
||||
|
||||
|
||||
def test_start_empty(self, sequential_threading):
|
||||
|
||||
context = {}
|
||||
config = {
|
||||
"short_breaks": [],
|
||||
"long_breaks": [],
|
||||
"short_break_interval": 15,
|
||||
"long_break_interval": 75,
|
||||
"long_break_duration": 60,
|
||||
"short_break_duration": 15,
|
||||
"random_order": False,
|
||||
"postpone_duration": 5,
|
||||
}
|
||||
on_update_next_break = mock.Mock()
|
||||
safe_eyes_core = core.SafeEyesCore(context)
|
||||
safe_eyes_core.on_update_next_break += mock
|
||||
|
||||
safe_eyes_core.initialize(config)
|
||||
|
||||
safe_eyes_core.start()
|
||||
safe_eyes_core.stop()
|
||||
|
||||
on_update_next_break.assert_not_called()
|
||||
|
||||
|
||||
def test_start(self, sequential_threading, time_machine):
|
||||
context = {
|
||||
"session": {},
|
||||
}
|
||||
config = {
|
||||
"short_breaks": [
|
||||
{"name": "break 1"},
|
||||
{"name": "break 2"},
|
||||
{"name": "break 3"},
|
||||
{"name": "break 4"},
|
||||
],
|
||||
"long_breaks": [
|
||||
{"name": "long break 1"},
|
||||
{"name": "long break 2"},
|
||||
{"name": "long break 3"},
|
||||
],
|
||||
"short_break_interval": 15,
|
||||
"long_break_interval": 75,
|
||||
"long_break_duration": 60,
|
||||
"short_break_duration": 15,
|
||||
"random_order": False,
|
||||
"postpone_duration": 5,
|
||||
}
|
||||
on_update_next_break = mock.Mock()
|
||||
safe_eyes_core = core.SafeEyesCore(context)
|
||||
safe_eyes_core.on_update_next_break += on_update_next_break
|
||||
|
||||
safe_eyes_core.initialize(config)
|
||||
|
||||
safe_eyes_core.start()
|
||||
|
||||
# start __scheduler_job
|
||||
sequential_threading.next()
|
||||
# FIXME: sleep is needed so code reaches the Condition
|
||||
sleep(0.1)
|
||||
assert context['state'] == model.State.WAITING
|
||||
|
||||
on_update_next_break.assert_called_once()
|
||||
assert isinstance(on_update_next_break.call_args[0][0], model.Break)
|
||||
assert on_update_next_break.call_args[0][0].name == "translated!: break 1"
|
||||
on_update_next_break.reset_mock()
|
||||
|
||||
with safe_eyes_core.lock:
|
||||
time_machine.shift(delta=datetime.timedelta(minutes=15))
|
||||
|
||||
with safe_eyes_core.waiting_condition:
|
||||
logging.debug("notify")
|
||||
safe_eyes_core.waiting_condition.notify_all()
|
||||
|
||||
logging.debug("wait for end of __scheduler_job")
|
||||
sequential_threading.wait()
|
||||
logging.debug("done waiting for end of __scheduler_job")
|
||||
|
||||
|
||||
safe_eyes_core.stop()
|
||||
assert context['state'] == model.State.STOPPED
|
||||
|
||||
logging.debug("done")
|
||||
|
||||
|
||||
def test_actual(self, sequential_threading, time_machine):
|
||||
context = {
|
||||
"session": {},
|
||||
}
|
||||
config = {
|
||||
"short_breaks": [
|
||||
{"name": "break 1"},
|
||||
{"name": "break 2"},
|
||||
{"name": "break 3"},
|
||||
{"name": "break 4"},
|
||||
],
|
||||
"long_breaks": [
|
||||
{"name": "long break 1"},
|
||||
{"name": "long break 2"},
|
||||
{"name": "long break 3"},
|
||||
],
|
||||
"short_break_interval": 15,
|
||||
"long_break_interval": 75,
|
||||
"long_break_duration": 60,
|
||||
"short_break_duration": 15,
|
||||
"pre_break_warning_time": 10,
|
||||
"random_order": False,
|
||||
"postpone_duration": 5,
|
||||
}
|
||||
on_update_next_break = mock.Mock()
|
||||
on_pre_break = mock.Mock(return_value=True)
|
||||
on_start_break = mock.Mock(return_value=True)
|
||||
start_break = mock.Mock()
|
||||
on_count_down = mock.Mock()
|
||||
safe_eyes_core = core.SafeEyesCore(context)
|
||||
safe_eyes_core.on_update_next_break += on_update_next_break
|
||||
safe_eyes_core.on_pre_break += on_pre_break
|
||||
safe_eyes_core.on_start_break += on_start_break
|
||||
safe_eyes_core.start_break += start_break
|
||||
safe_eyes_core.on_count_down += on_count_down
|
||||
|
||||
safe_eyes_core.initialize(config)
|
||||
|
||||
safe_eyes_core.start()
|
||||
|
||||
# start __scheduler_job
|
||||
sequential_threading.next()
|
||||
# FIXME: sleep is needed so code reaches the Condition
|
||||
sleep(0.1)
|
||||
assert context['state'] == model.State.WAITING
|
||||
|
||||
on_update_next_break.assert_called_once()
|
||||
assert isinstance(on_update_next_break.call_args[0][0], model.Break)
|
||||
assert on_update_next_break.call_args[0][0].name == "translated!: break 1"
|
||||
on_update_next_break.reset_mock()
|
||||
|
||||
with safe_eyes_core.lock:
|
||||
time_machine.shift(delta=datetime.timedelta(minutes=15))
|
||||
|
||||
with safe_eyes_core.waiting_condition:
|
||||
logging.debug("notify")
|
||||
safe_eyes_core.waiting_condition.notify_all()
|
||||
|
||||
logging.debug("wait for end of __scheduler_job")
|
||||
sequential_threading.wait()
|
||||
logging.debug("done waiting for end of __scheduler_job")
|
||||
|
||||
assert context['state'] == model.State.PRE_BREAK
|
||||
|
||||
on_pre_break.assert_called_once()
|
||||
assert isinstance(on_pre_break.call_args[0][0], model.Break)
|
||||
assert on_pre_break.call_args[0][0].name == "translated!: break 1"
|
||||
on_pre_break.reset_mock()
|
||||
|
||||
# start __wait_until_prepare
|
||||
sequential_threading.next()
|
||||
|
||||
# FIXME: sleep is needed so code reaches the Condition
|
||||
sleep(0.1)
|
||||
with safe_eyes_core.lock:
|
||||
time_machine.shift(delta=datetime.timedelta(seconds=10))
|
||||
|
||||
with safe_eyes_core.waiting_condition:
|
||||
logging.debug("notify")
|
||||
safe_eyes_core.waiting_condition.notify_all()
|
||||
|
||||
logging.debug("wait for end of __wait_until_prepare")
|
||||
sequential_threading.wait()
|
||||
logging.debug("done waiting for end of __wait_until_prepare")
|
||||
|
||||
# start __start_break
|
||||
sequential_threading.next()
|
||||
sequential_threading.wait()
|
||||
|
||||
# first sleep in __start_break
|
||||
sequential_threading.next()
|
||||
|
||||
on_start_break.assert_called_once()
|
||||
assert isinstance(on_start_break.call_args[0][0], model.Break)
|
||||
assert on_start_break.call_args[0][0].name == "translated!: break 1"
|
||||
on_start_break.reset_mock()
|
||||
|
||||
start_break.assert_called_once()
|
||||
assert isinstance(start_break.call_args[0][0], model.Break)
|
||||
assert start_break.call_args[0][0].name == "translated!: break 1"
|
||||
start_break.reset_mock()
|
||||
|
||||
assert context['state'] == model.State.BREAK
|
||||
|
||||
# continue sleep in __start_break
|
||||
for i in range(config["short_break_duration"] - 1):
|
||||
sequential_threading.wait()
|
||||
sequential_threading.next()
|
||||
|
||||
logging.debug("wait for end of __start_break")
|
||||
sequential_threading.wait()
|
||||
logging.debug("done waiting for end of __start_break")
|
||||
|
||||
on_count_down.assert_called()
|
||||
assert on_count_down.call_count == 15
|
||||
on_count_down.reset_mock()
|
||||
|
||||
assert context['state'] == model.State.BREAK
|
||||
|
||||
safe_eyes_core.stop()
|
||||
|
||||
on_update_next_break.assert_not_called()
|
||||
on_pre_break.assert_not_called()
|
||||
on_start_break.assert_not_called()
|
||||
start_break.assert_not_called()
|
||||
assert context['state'] == model.State.STOPPED
|
Loading…
Reference in New Issue