mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Merge branch 'master' of https://github.com/davewiththenicehat/evennia into davewiththenicehat-master
This commit is contained in:
commit
cec566be79
4 changed files with 695 additions and 56 deletions
|
|
@ -70,11 +70,11 @@ class SlowExit(DefaultExit):
|
|||
|
||||
traversing_object.msg("You start moving %s at a %s." % (self.key, move_speed))
|
||||
# create a delayed movement
|
||||
deferred = utils.delay(move_delay, move_callback)
|
||||
t = utils.delay(move_delay, move_callback)
|
||||
# we store the deferred on the character, this will allow us
|
||||
# to abort the movement. We must use an ndb here since
|
||||
# deferreds cannot be pickled.
|
||||
traversing_object.ndb.currently_moving = deferred
|
||||
traversing_object.ndb.currently_moving = t
|
||||
|
||||
|
||||
#
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from datetime import datetime, timedelta
|
|||
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet.task import deferLater
|
||||
from twisted.internet.defer import CancelledError as DefCancelledError
|
||||
from evennia.server.models import ServerConfig
|
||||
from evennia.utils.logger import log_err
|
||||
from evennia.utils.dbserialize import dbserialize, dbunserialize
|
||||
|
|
@ -13,31 +14,213 @@ from evennia.utils.dbserialize import dbserialize, dbunserialize
|
|||
TASK_HANDLER = None
|
||||
|
||||
|
||||
class TaskHandler(object):
|
||||
def handle_error(*args, **kwargs):
|
||||
"""Handle errors within deferred objects."""
|
||||
for arg in args:
|
||||
# suppress cancel errors
|
||||
if arg.type == DefCancelledError:
|
||||
continue
|
||||
raise arg
|
||||
|
||||
|
||||
class TaskHandlerTask:
|
||||
"""An object to represent a single TaskHandler task.
|
||||
|
||||
Instance Attributes:
|
||||
task_id (int): the global id for this task
|
||||
deferred (deferred): a reference to this task's deferred
|
||||
Property Attributes:
|
||||
paused (bool): check if the deferred instance of a task has been paused.
|
||||
called(self): A task attribute to check if the deferred instance of a task has been called.
|
||||
|
||||
Methods:
|
||||
pause(): Pause the callback of a task.
|
||||
unpause(): Process all callbacks made since pause() was called.
|
||||
do_task(): Execute the task (call its callback).
|
||||
call(): Call the callback of this task.
|
||||
remove(): Remove a task without executing it.
|
||||
cancel(): Stop a task from automatically executing.
|
||||
active(): Check if a task is active (has not been called yet).
|
||||
exists(): Check if a task exists.
|
||||
get_id(): Returns the global id for this task. For use with
|
||||
|
||||
"""
|
||||
A light singleton wrapper allowing to access permanent tasks.
|
||||
|
||||
def __init__(self, task_id):
|
||||
self.task_id = task_id
|
||||
self.deferred = TASK_HANDLER.get_deferred(task_id)
|
||||
|
||||
def get_deferred(self):
|
||||
"""Return the instance of the deferred the task id is using.
|
||||
|
||||
Returns:
|
||||
bool or deferred: An instance of a deferred or False if there is no task with the id.
|
||||
None is returned if there is no deferred affiliated with this id.
|
||||
|
||||
"""
|
||||
return TASK_HANDLER.get_deferred(self.task_id)
|
||||
|
||||
def pause(self):
|
||||
"""Pause the callback of a task.
|
||||
To resume use TaskHandlerTask.unpause
|
||||
"""
|
||||
d = self.deferred
|
||||
if d:
|
||||
d.pause()
|
||||
|
||||
def unpause(self):
|
||||
"""Unpause a task, run the task if it has passed delay time."""
|
||||
d = self.deferred
|
||||
if d:
|
||||
d.unpause()
|
||||
|
||||
@property
|
||||
def paused(self):
|
||||
"""A task attribute to check if the deferred instance of a task has been paused.
|
||||
|
||||
This exists to mock usage of a twisted deferred object.
|
||||
|
||||
Returns:
|
||||
bool or None: True if the task was properly paused. None if the task does not have
|
||||
a deferred instance.
|
||||
|
||||
"""
|
||||
d = self.deferred
|
||||
if d:
|
||||
return d.paused
|
||||
else:
|
||||
return None
|
||||
|
||||
def do_task(self):
|
||||
"""Execute the task (call its callback).
|
||||
If calling before timedelay, cancel the deferred instance affliated to this task.
|
||||
Remove the task from the dictionary of current tasks on a successful
|
||||
callback.
|
||||
|
||||
Returns:
|
||||
bool or any: Set to `False` if the task does not exist in task
|
||||
handler. Otherwise it will be the return of the task's callback.
|
||||
|
||||
"""
|
||||
return TASK_HANDLER.do_task(self.task_id)
|
||||
|
||||
def call(self):
|
||||
"""Call the callback of a task.
|
||||
Leave the task unaffected otherwise.
|
||||
This does not use the task's deferred instance.
|
||||
The only requirement is that the task exist in task handler.
|
||||
|
||||
Returns:
|
||||
bool or any: Set to `False` if the task does not exist in task
|
||||
handler. Otherwise it will be the return of the task's callback.
|
||||
|
||||
"""
|
||||
return TASK_HANDLER.call_task(self.task_id)
|
||||
|
||||
def remove(self):
|
||||
"""Remove a task without executing it.
|
||||
Deletes the instance of the task's deferred.
|
||||
|
||||
Args:
|
||||
task_id (int): an existing task ID.
|
||||
|
||||
Returns:
|
||||
bool: True if the removal completed successfully.
|
||||
|
||||
"""
|
||||
return TASK_HANDLER.remove(self.task_id)
|
||||
|
||||
def cancel(self):
|
||||
"""Stop a task from automatically executing.
|
||||
This will not remove the task.
|
||||
|
||||
Returns:
|
||||
bool: True if the cancel completed successfully.
|
||||
False if the cancel did not complete successfully.
|
||||
|
||||
"""
|
||||
return TASK_HANDLER.cancel(self.task_id)
|
||||
|
||||
def active(self):
|
||||
"""Check if a task is active (has not been called yet).
|
||||
|
||||
Returns:
|
||||
bool: True if a task is active (has not been called yet). False if
|
||||
it is not (has been called) or if the task does not exist.
|
||||
|
||||
"""
|
||||
return TASK_HANDLER.active(self.task_id)
|
||||
|
||||
@property
|
||||
def called(self):
|
||||
"""
|
||||
A task attribute to check if the deferred instance of a task has been called.
|
||||
|
||||
This exists to mock usage of a twisted deferred object.
|
||||
It will not set to True if Task.call has been called. This only happens if
|
||||
task's deferred instance calls the callback.
|
||||
|
||||
Returns:
|
||||
bool: True if the deferred instance of this task has called the callback.
|
||||
False if the deferred instnace of this task has not called the callback.
|
||||
|
||||
"""
|
||||
d = self.deferred
|
||||
if d:
|
||||
return d.called
|
||||
else:
|
||||
return None
|
||||
|
||||
def exists(self):
|
||||
"""Check if a task exists.
|
||||
Most task handler methods check for existence for you.
|
||||
|
||||
Returns:
|
||||
bool: True the task exists False if it does not.
|
||||
|
||||
"""
|
||||
return TASK_HANDLER.exists(self.task_id)
|
||||
|
||||
def get_id(self):
|
||||
""" Returns the global id for this task. For use with
|
||||
`evennia.scripts.taskhandler.TASK_HANDLER`.
|
||||
|
||||
Returns:
|
||||
task_id (int): global task id for this task.
|
||||
|
||||
"""
|
||||
return self.task_id
|
||||
|
||||
|
||||
class TaskHandler(object):
|
||||
|
||||
"""A light singleton wrapper allowing to access permanent tasks.
|
||||
|
||||
When `utils.delay` is called, the task handler is used to create
|
||||
the task. If `utils.delay` is called with `persistent=True`, the
|
||||
task handler stores the new task and saves.
|
||||
the task.
|
||||
|
||||
It's easier to access these tasks (should it be necessary) using
|
||||
`evennia.scripts.taskhandler.TASK_HANDLER`, which contains one
|
||||
instance of this class, and use its `add` and `remove` methods.
|
||||
Task handler will automatically remove uncalled but canceled from task
|
||||
handler. By default this will not occur until a canceled task
|
||||
has been uncalled for 60 second after the time it should have been called.
|
||||
To adjust this time use TASK_HANDLER.stale_timeout. If stale_timeout is 0
|
||||
stale tasks will not be automatically removed.
|
||||
This is not done on a timer. I is done as new tasks are added or the load method is called.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.tasks = {}
|
||||
self.to_save = {}
|
||||
self.clock = reactor
|
||||
# number of seconds before an uncalled canceled task is removed from TaskHandler
|
||||
self.stale_timeout = 60
|
||||
self._now = False # used in unit testing to manually set now time
|
||||
|
||||
def load(self):
|
||||
"""Load from the ServerConfig.
|
||||
|
||||
Note:
|
||||
This should be automatically called when Evennia starts.
|
||||
It populates `self.tasks` according to the ServerConfig.
|
||||
This should be automatically called when Evennia starts.
|
||||
It populates `self.tasks` according to the ServerConfig.
|
||||
|
||||
"""
|
||||
to_save = False
|
||||
|
|
@ -58,16 +241,42 @@ class TaskHandler(object):
|
|||
continue
|
||||
|
||||
callback = getattr(obj, method)
|
||||
self.tasks[task_id] = (date, callback, args, kwargs)
|
||||
self.tasks[task_id] = (date, callback, args, kwargs, True, None)
|
||||
|
||||
if self.stale_timeout > 0: # cleanup stale tasks.
|
||||
self.clean_stale_tasks()
|
||||
if to_save:
|
||||
self.save()
|
||||
|
||||
def clean_stale_tasks(self):
|
||||
"""remove uncalled but canceled from task handler.
|
||||
|
||||
By default this will not occur until a canceled task
|
||||
has been uncalled for 60 second after the time it should have been called.
|
||||
To adjust this time use TASK_HANDLER.stale_timeout.
|
||||
|
||||
"""
|
||||
clean_ids = []
|
||||
for task_id, (date, callback, args, kwargs, persistent, _) in self.tasks.items():
|
||||
if not self.active(task_id):
|
||||
stale_date = date + timedelta(seconds=self.stale_timeout)
|
||||
# if a now time is provided use it (intended for unit testing)
|
||||
now = self._now if self._now else datetime.now()
|
||||
# the task was canceled more than stale_timeout seconds ago
|
||||
if now > stale_date:
|
||||
clean_ids.append(task_id)
|
||||
for task_id in clean_ids:
|
||||
self.remove(task_id)
|
||||
return True
|
||||
|
||||
def save(self):
|
||||
"""Save the tasks in ServerConfig."""
|
||||
for task_id, (date, callback, args, kwargs) in self.tasks.items():
|
||||
|
||||
for task_id, (date, callback, args, kwargs, persistent, _) in self.tasks.items():
|
||||
if task_id in self.to_save:
|
||||
continue
|
||||
if not persistent:
|
||||
continue
|
||||
|
||||
if getattr(callback, "__self__", None):
|
||||
# `callback` is an instance method
|
||||
|
|
@ -93,31 +302,49 @@ class TaskHandler(object):
|
|||
ServerConfig.objects.conf("delayed_tasks", self.to_save)
|
||||
|
||||
def add(self, timedelay, callback, *args, **kwargs):
|
||||
"""Add a new persistent task in the configuration.
|
||||
"""Add a new task.
|
||||
|
||||
If the persistent kwarg is truthy:
|
||||
The callback, args and values for kwarg will be serialized. Type
|
||||
and attribute errors during the serialization will be logged,
|
||||
but will not throw exceptions.
|
||||
For persistent tasks do not use memory references in the callback
|
||||
function or arguments. After a restart those memory references are no
|
||||
longer accurate.
|
||||
|
||||
Args:
|
||||
timedelay (int or float): time in sedconds before calling the callback.
|
||||
timedelay (int or float): time in seconds before calling the callback.
|
||||
callback (function or instance method): the callback itself
|
||||
any (any): any additional positional arguments to send to the callback
|
||||
*args: positional arguments to pass to callback.
|
||||
**kwargs: keyword arguments to pass to callback.
|
||||
persistent (bool, optional): persist the task (stores it).
|
||||
persistent key and value is removed from kwargs it will
|
||||
not be passed to callback.
|
||||
|
||||
Keyword Args:
|
||||
persistent (bool, optional): persist the task (store it).
|
||||
any (any): additional keyword arguments to send to the callback
|
||||
Returns:
|
||||
TaskHandlerTask: An object to represent a task.
|
||||
Reference evennia.scripts.taskhandler.TaskHandlerTask for complete details.
|
||||
|
||||
"""
|
||||
persistent = kwargs.get("persistent", False)
|
||||
if persistent:
|
||||
del kwargs["persistent"]
|
||||
now = datetime.now()
|
||||
delta = timedelta(seconds=timedelay)
|
||||
# set the completion time
|
||||
# Only used on persistent tasks after a restart
|
||||
now = datetime.now()
|
||||
delta = timedelta(seconds=timedelay)
|
||||
comp_time = now + delta
|
||||
# get an open task id
|
||||
used_ids = list(self.tasks.keys())
|
||||
task_id = 1
|
||||
while task_id in used_ids:
|
||||
task_id += 1
|
||||
|
||||
# Choose a free task_id
|
||||
# record the task to the tasks dictionary
|
||||
persistent = kwargs.get("persistent", False)
|
||||
if "persistent" in kwargs:
|
||||
del kwargs["persistent"]
|
||||
if persistent:
|
||||
safe_args = []
|
||||
safe_kwargs = {}
|
||||
used_ids = list(self.tasks.keys())
|
||||
task_id = 1
|
||||
while task_id in used_ids:
|
||||
task_id += 1
|
||||
|
||||
# Check that args and kwargs contain picklable information
|
||||
for arg in args:
|
||||
|
|
@ -144,59 +371,219 @@ class TaskHandler(object):
|
|||
else:
|
||||
safe_kwargs[key] = value
|
||||
|
||||
self.tasks[task_id] = (now + delta, callback, safe_args, safe_kwargs)
|
||||
self.tasks[task_id] = (comp_time, callback, safe_args, safe_kwargs, persistent, None)
|
||||
self.save()
|
||||
callback = self.do_task
|
||||
args = [task_id]
|
||||
kwargs = {}
|
||||
else: # this is a non-persitent task
|
||||
self.tasks[task_id] = (comp_time, callback, args, kwargs, persistent, None)
|
||||
|
||||
return deferLater(reactor, timedelay, callback, *args, **kwargs)
|
||||
# defer the task
|
||||
callback = self.do_task
|
||||
args = [task_id]
|
||||
kwargs = {}
|
||||
d = deferLater(self.clock, timedelay, callback, *args, **kwargs)
|
||||
d.addErrback(handle_error)
|
||||
|
||||
def remove(self, task_id):
|
||||
"""Remove a persistent task without executing it.
|
||||
# some tasks may complete before the deferred can be added
|
||||
if task_id in self.tasks:
|
||||
task = self.tasks.get(task_id)
|
||||
task = list(task)
|
||||
task[4] = persistent
|
||||
task[5] = d
|
||||
self.tasks[task_id] = task
|
||||
else: # the task already completed
|
||||
return False
|
||||
if self.stale_timeout > 0:
|
||||
self.clean_stale_tasks()
|
||||
return TaskHandlerTask(task_id)
|
||||
|
||||
def exists(self, task_id):
|
||||
"""Check if a task exists.
|
||||
Most task handler methods check for existence for you.
|
||||
|
||||
Args:
|
||||
task_id (int): an existing task ID.
|
||||
|
||||
Note:
|
||||
A non-persistent task doesn't have a task_id, it is not stored
|
||||
in the TaskHandler.
|
||||
Returns:
|
||||
bool: True the task exists False if it does not.
|
||||
|
||||
"""
|
||||
del self.tasks[task_id]
|
||||
if task_id in self.tasks:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def active(self, task_id):
|
||||
"""Check if a task is active (has not been called yet).
|
||||
|
||||
Args:
|
||||
task_id (int): an existing task ID.
|
||||
|
||||
Returns:
|
||||
bool: True if a task is active (has not been called yet). False if
|
||||
it is not (has been called) or if the task does not exist.
|
||||
|
||||
"""
|
||||
if task_id in self.tasks:
|
||||
# if the task has not been run, cancel it
|
||||
deferred = self.get_deferred(task_id)
|
||||
return not (deferred and deferred.called)
|
||||
else:
|
||||
return False
|
||||
|
||||
def cancel(self, task_id):
|
||||
"""Stop a task from automatically executing.
|
||||
This will not remove the task.
|
||||
|
||||
Args:
|
||||
task_id (int): an existing task ID.
|
||||
|
||||
Returns:
|
||||
bool: True if the cancel completed successfully.
|
||||
False if the cancel did not complete successfully.
|
||||
|
||||
"""
|
||||
if task_id in self.tasks:
|
||||
# if the task has not been run, cancel it
|
||||
d = self.get_deferred(task_id)
|
||||
if d: # it is remotely possible for a task to not have a deferred
|
||||
if d.called:
|
||||
return False
|
||||
else: # the callback has not been called yet.
|
||||
d.cancel()
|
||||
return True
|
||||
else: # this task has no deferred instance
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
def remove(self, task_id):
|
||||
"""Remove a task without executing it.
|
||||
Deletes the instance of the task's deferred.
|
||||
|
||||
Args:
|
||||
task_id (int): an existing task ID.
|
||||
|
||||
Returns:
|
||||
bool: True if the removal completed successfully.
|
||||
|
||||
"""
|
||||
d = None
|
||||
# delete the task from the tasks dictionary
|
||||
if task_id in self.tasks:
|
||||
# if the task has not been run, cancel it
|
||||
self.cancel(task_id)
|
||||
del self.tasks[task_id] # delete the task from the tasks dictionary
|
||||
# remove the task from the persistent dictionary and ServerConfig
|
||||
if task_id in self.to_save:
|
||||
del self.to_save[task_id]
|
||||
self.save() # remove from ServerConfig.objects
|
||||
# delete the instance of the deferred
|
||||
if d:
|
||||
del d
|
||||
return True
|
||||
|
||||
self.save()
|
||||
def clear(self, save=True, cancel=True):
|
||||
"""clear all tasks.
|
||||
By default tasks are canceled and removed from the database also.
|
||||
|
||||
Args:
|
||||
save=True (bool): Should changes to persistent tasks be saved to database.
|
||||
cancel=True (bool): Cancel scheduled tasks before removing it from task handler.
|
||||
|
||||
Returns:
|
||||
True (bool): if the removal completed successfully.
|
||||
|
||||
"""
|
||||
if self.tasks:
|
||||
for task_id in self.tasks.keys():
|
||||
if cancel:
|
||||
self.cancel(task_id)
|
||||
self.tasks = {}
|
||||
if self.to_save:
|
||||
self.to_save = {}
|
||||
if save:
|
||||
self.save()
|
||||
return True
|
||||
|
||||
def call_task(self, task_id):
|
||||
"""Call the callback of a task.
|
||||
Leave the task unaffected otherwise.
|
||||
This does not use the task's deferred instance.
|
||||
The only requirement is that the task exist in task handler.
|
||||
|
||||
Args:
|
||||
task_id (int): an existing task ID.
|
||||
|
||||
Returns:
|
||||
bool or any: Set to `False` if the task does not exist in task
|
||||
handler. Otherwise it will be the return of the task's callback.
|
||||
|
||||
"""
|
||||
if task_id in self.tasks:
|
||||
date, callback, args, kwargs, persistent, d = self.tasks.get(task_id)
|
||||
else: # the task does not exist
|
||||
return False
|
||||
return callback(*args, **kwargs)
|
||||
|
||||
def do_task(self, task_id):
|
||||
"""Execute the task (call its callback).
|
||||
If calling before timedelay cancel the deferred instance affliated to this task.
|
||||
Remove the task from the dictionary of current tasks on a successful
|
||||
callback.
|
||||
|
||||
Args:
|
||||
task_id (int): a valid task ID.
|
||||
|
||||
Note:
|
||||
This will also remove it from the list of current tasks.
|
||||
Returns:
|
||||
bool or any: Set to `False` if the task does not exist in task
|
||||
handler. Otherwise it will be the return of the task's callback.
|
||||
|
||||
"""
|
||||
date, callback, args, kwargs = self.tasks.pop(task_id)
|
||||
if task_id in self.to_save:
|
||||
del self.to_save[task_id]
|
||||
callback_return = False
|
||||
if task_id in self.tasks:
|
||||
date, callback, args, kwargs, persistent, d = self.tasks.get(task_id)
|
||||
else: # the task does not exist
|
||||
return False
|
||||
if d: # it is remotely possible for a task to not have a deferred
|
||||
if not d.called: # the task's deferred has not been called yet
|
||||
d.cancel() # cancel the automated callback
|
||||
else: # this task has no deferred, and should not be called
|
||||
return False
|
||||
callback_return = callback(*args, **kwargs)
|
||||
self.remove(task_id)
|
||||
return callback_return
|
||||
|
||||
self.save()
|
||||
callback(*args, **kwargs)
|
||||
def get_deferred(self, task_id):
|
||||
"""
|
||||
Return the instance of the deferred the task id is using.
|
||||
|
||||
Args:
|
||||
task_id (int): a valid task ID.
|
||||
|
||||
Returns:
|
||||
bool or deferred: An instance of a deferred or False if there is no task with the id.
|
||||
None is returned if there is no deferred affiliated with this id.
|
||||
|
||||
"""
|
||||
if task_id in self.tasks:
|
||||
return self.tasks[task_id][5]
|
||||
else:
|
||||
return None
|
||||
|
||||
def create_delays(self):
|
||||
"""Create the delayed tasks for the persistent tasks.
|
||||
|
||||
Note:
|
||||
This method should be automatically called when Evennia starts.
|
||||
This method should be automatically called when Evennia starts.
|
||||
|
||||
"""
|
||||
now = datetime.now()
|
||||
for task_id, (date, callbac, args, kwargs) in self.tasks.items():
|
||||
for task_id, (date, callback, args, kwargs, _, _) in self.tasks.items():
|
||||
self.tasks[task_id] = date, callback, args, kwargs, True, None
|
||||
seconds = max(0, (date - now).total_seconds())
|
||||
deferLater(reactor, seconds, self.do_task, task_id)
|
||||
d = deferLater(self.clock, seconds, self.do_task, task_id)
|
||||
d.addErrback(handle_error)
|
||||
# some tasks may complete before the deferred can be added
|
||||
if self.tasks.get(task_id, False):
|
||||
self.tasks[task_id] = date, callback, args, kwargs, True, d
|
||||
|
||||
|
||||
# Create the soft singleton
|
||||
|
|
|
|||
|
|
@ -6,13 +6,16 @@ TODO: Not nearly all utilities are covered yet.
|
|||
"""
|
||||
|
||||
import os.path
|
||||
|
||||
import mock
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.test import TestCase
|
||||
from datetime import datetime
|
||||
from twisted.internet import task
|
||||
|
||||
from evennia.utils.ansi import ANSIString
|
||||
from evennia.utils import utils
|
||||
from evennia.utils.test_resources import EvenniaTest
|
||||
|
||||
|
||||
class TestIsIter(TestCase):
|
||||
|
|
@ -292,3 +295,241 @@ class LatinifyTest(TestCase):
|
|||
byte_str = utils.to_bytes(self.example_str)
|
||||
result = utils.latinify(byte_str)
|
||||
self.assertEqual(result, self.expected_output)
|
||||
|
||||
|
||||
_TASK_HANDLER = None
|
||||
|
||||
|
||||
def dummy_func(obj):
|
||||
"""
|
||||
Used in TestDelay.
|
||||
|
||||
A function that:
|
||||
can be serialized
|
||||
uses no memory references
|
||||
uses evennia objects
|
||||
"""
|
||||
# get a reference of object
|
||||
from evennia.objects.models import ObjectDB
|
||||
obj = ObjectDB.objects.object_search(obj)
|
||||
obj = obj[0]
|
||||
# make changes to object
|
||||
obj.ndb.dummy_var = 'dummy_func ran'
|
||||
return True
|
||||
|
||||
|
||||
class TestDelay(EvenniaTest):
|
||||
"""
|
||||
Test utils.delay.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
# get a reference of TASK_HANDLER
|
||||
self.timedelay = 5
|
||||
global _TASK_HANDLER
|
||||
if _TASK_HANDLER is None:
|
||||
from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER
|
||||
_TASK_HANDLER.clock = task.Clock()
|
||||
self.char1.ndb.dummy_var = False
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
_TASK_HANDLER.clear()
|
||||
|
||||
def test_call_early(self):
|
||||
# call a task early with call
|
||||
for pers in (True, False):
|
||||
t = utils.delay(self.timedelay, dummy_func, self.char1.dbref, persistent=pers)
|
||||
result = t.call()
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran')
|
||||
self.assertTrue(t.exists())
|
||||
self.assertTrue(t.active())
|
||||
self.char1.ndb.dummy_var = False
|
||||
|
||||
def test_do_task(self):
|
||||
# call the task early with do_task
|
||||
for pers in (True, False):
|
||||
t = utils.delay(self.timedelay, dummy_func, self.char1.dbref, persistent=pers)
|
||||
# call the task early to test Task.call and TaskHandler.call_task
|
||||
result = t.do_task()
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran')
|
||||
self.assertFalse(t.exists())
|
||||
self.char1.ndb.dummy_var = False
|
||||
|
||||
def test_deferred_call(self):
|
||||
# wait for deferred to call
|
||||
timedelay = self.timedelay
|
||||
for pers in (False, True):
|
||||
t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=pers)
|
||||
self.assertTrue(t.active())
|
||||
_TASK_HANDLER.clock.advance(timedelay) # make time pass
|
||||
self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran')
|
||||
self.assertFalse(t.exists())
|
||||
self.char1.ndb.dummy_var = False
|
||||
|
||||
def test_short_deferred_call(self):
|
||||
# wait for deferred to call with a very short time
|
||||
timedelay = .1
|
||||
for pers in (False, True):
|
||||
t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=pers)
|
||||
self.assertTrue(t.active())
|
||||
_TASK_HANDLER.clock.advance(timedelay) # make time pass
|
||||
self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran')
|
||||
self.assertFalse(t.exists())
|
||||
self.char1.ndb.dummy_var = False
|
||||
|
||||
def test_active(self):
|
||||
timedelay = self.timedelay
|
||||
t = utils.delay(timedelay, dummy_func, self.char1.dbref)
|
||||
self.assertTrue(_TASK_HANDLER.active(t.get_id()))
|
||||
self.assertTrue(t.active())
|
||||
_TASK_HANDLER.clock.advance(timedelay) # make time pass
|
||||
self.assertFalse(_TASK_HANDLER.active(t.get_id()))
|
||||
self.assertFalse(t.active())
|
||||
|
||||
def test_called(self):
|
||||
timedelay = self.timedelay
|
||||
t = utils.delay(timedelay, dummy_func, self.char1.dbref)
|
||||
self.assertFalse(t.called)
|
||||
_TASK_HANDLER.clock.advance(timedelay) # make time pass
|
||||
self.assertTrue(t.called)
|
||||
|
||||
def test_cancel(self):
|
||||
timedelay = self.timedelay
|
||||
for pers in (False, True):
|
||||
t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=pers)
|
||||
self.assertTrue(t.active())
|
||||
success = t.cancel()
|
||||
self.assertFalse(t.active())
|
||||
self.assertTrue(success)
|
||||
self.assertTrue(t.exists())
|
||||
_TASK_HANDLER.clock.advance(timedelay) # make time pass
|
||||
self.assertEqual(self.char1.ndb.dummy_var, False)
|
||||
|
||||
def test_remove(self):
|
||||
timedelay = self.timedelay
|
||||
for pers in (False, True):
|
||||
t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=pers)
|
||||
self.assertTrue(t.active())
|
||||
success = t.remove()
|
||||
self.assertTrue(success)
|
||||
self.assertFalse(t.active())
|
||||
self.assertFalse(t.exists())
|
||||
_TASK_HANDLER.clock.advance(timedelay) # make time pass
|
||||
self.assertEqual(self.char1.ndb.dummy_var, False)
|
||||
|
||||
def test_remove_canceled(self):
|
||||
# remove a canceled task
|
||||
timedelay = self.timedelay
|
||||
for pers in (False, True):
|
||||
t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=pers)
|
||||
self.assertTrue(t.active())
|
||||
success = t.cancel()
|
||||
self.assertTrue(success)
|
||||
self.assertTrue(t.exists())
|
||||
self.assertFalse(t.active())
|
||||
success = t.remove()
|
||||
self.assertTrue(success)
|
||||
self.assertFalse(t.exists())
|
||||
_TASK_HANDLER.clock.advance(timedelay) # make time pass
|
||||
self.assertEqual(self.char1.ndb.dummy_var, False)
|
||||
|
||||
def test_pause_unpause(self):
|
||||
# remove a canceled task
|
||||
timedelay = self.timedelay
|
||||
for pers in (False, True):
|
||||
t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=pers)
|
||||
self.assertTrue(t.active())
|
||||
t.pause()
|
||||
self.assertTrue(t.paused)
|
||||
t.unpause()
|
||||
self.assertFalse(t.paused)
|
||||
self.assertEqual(self.char1.ndb.dummy_var, False)
|
||||
t.pause()
|
||||
_TASK_HANDLER.clock.advance(timedelay) # make time pass
|
||||
self.assertEqual(self.char1.ndb.dummy_var, False)
|
||||
t.unpause()
|
||||
self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran')
|
||||
self.char1.ndb.dummy_var = False
|
||||
|
||||
def test_auto_stale_task_removal(self):
|
||||
# automated removal of stale tasks.
|
||||
timedelay = self.timedelay
|
||||
for pers in (False, True):
|
||||
t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=pers)
|
||||
t.cancel()
|
||||
self.assertFalse(t.active())
|
||||
_TASK_HANDLER.clock.advance(timedelay) # make time pass
|
||||
if pers:
|
||||
self.assertTrue(t.get_id() in _TASK_HANDLER.to_save)
|
||||
self.assertTrue(t.get_id() in _TASK_HANDLER.tasks)
|
||||
# Make task handler's now time, after the stale timeout
|
||||
_TASK_HANDLER._now = datetime.now() + timedelta(seconds=_TASK_HANDLER.stale_timeout + timedelay + 1)
|
||||
# add a task to test automatic removal
|
||||
t2 = utils.delay(timedelay, dummy_func, self.char1.dbref)
|
||||
if pers:
|
||||
self.assertFalse(t.get_id() in _TASK_HANDLER.to_save)
|
||||
self.assertFalse(t.get_id() in _TASK_HANDLER.tasks)
|
||||
self.assertEqual(self.char1.ndb.dummy_var, False)
|
||||
_TASK_HANDLER.clear()
|
||||
|
||||
def test_manual_stale_task_removal(self):
|
||||
# manual removal of stale tasks.
|
||||
timedelay = self.timedelay
|
||||
for pers in (False, True):
|
||||
t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=pers)
|
||||
t.cancel()
|
||||
self.assertFalse(t.active())
|
||||
_TASK_HANDLER.clock.advance(timedelay) # make time pass
|
||||
if pers:
|
||||
self.assertTrue(t.get_id() in _TASK_HANDLER.to_save)
|
||||
self.assertTrue(t.get_id() in _TASK_HANDLER.tasks)
|
||||
# Make task handler's now time, after the stale timeout
|
||||
_TASK_HANDLER._now = datetime.now() + timedelta(seconds=_TASK_HANDLER.stale_timeout + timedelay + 1)
|
||||
_TASK_HANDLER.clean_stale_tasks() # cleanup of stale tasks in in the save method
|
||||
if pers:
|
||||
self.assertFalse(t.get_id() in _TASK_HANDLER.to_save)
|
||||
self.assertFalse(t.get_id() in _TASK_HANDLER.tasks)
|
||||
self.assertEqual(self.char1.ndb.dummy_var, False)
|
||||
_TASK_HANDLER.clear()
|
||||
|
||||
def test_disable_stale_removal(self):
|
||||
# manual removal of stale tasks.
|
||||
timedelay = self.timedelay
|
||||
_TASK_HANDLER.stale_timeout = 0
|
||||
for pers in (False, True):
|
||||
t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=pers)
|
||||
t.cancel()
|
||||
self.assertFalse(t.active())
|
||||
_TASK_HANDLER.clock.advance(timedelay) # make time pass
|
||||
if pers:
|
||||
self.assertTrue(t.get_id() in _TASK_HANDLER.to_save)
|
||||
self.assertTrue(t.get_id() in _TASK_HANDLER.tasks)
|
||||
# Make task handler's now time, after the stale timeout
|
||||
_TASK_HANDLER._now = datetime.now() + timedelta(seconds=_TASK_HANDLER.stale_timeout + timedelay + 1)
|
||||
t2 = utils.delay(timedelay, dummy_func, self.char1.dbref)
|
||||
if pers:
|
||||
self.assertTrue(t.get_id() in _TASK_HANDLER.to_save)
|
||||
self.assertTrue(t.get_id() in _TASK_HANDLER.tasks)
|
||||
self.assertEqual(self.char1.ndb.dummy_var, False)
|
||||
# manual removal should still work
|
||||
_TASK_HANDLER.clean_stale_tasks() # cleanup of stale tasks in in the save method
|
||||
if pers:
|
||||
self.assertFalse(t.get_id() in _TASK_HANDLER.to_save)
|
||||
self.assertFalse(t.get_id() in _TASK_HANDLER.tasks)
|
||||
_TASK_HANDLER.clear()
|
||||
|
||||
def test_server_restart(self):
|
||||
# emulate a server restart
|
||||
timedelay = self.timedelay
|
||||
utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True)
|
||||
_TASK_HANDLER.clear(False) # remove all tasks from task handler, do not save this change.
|
||||
_TASK_HANDLER.clock.advance(timedelay) # advance twisted reactor time past callback time
|
||||
self.assertEqual(self.char1.ndb.dummy_var, False) # task has not run
|
||||
_TASK_HANDLER.load() # load persistent tasks from database.
|
||||
_TASK_HANDLER.create_delays() # create new deffered instances from persistent tasks
|
||||
_TASK_HANDLER.clock.advance(timedelay) # Clock must advance to trigger, even if past timedelay
|
||||
self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran')
|
||||
|
|
|
|||
|
|
@ -1022,7 +1022,7 @@ _TASK_HANDLER = None
|
|||
|
||||
def delay(timedelay, callback, *args, **kwargs):
|
||||
"""
|
||||
Delay the return of a value.
|
||||
Delay the calling of a callback (function).
|
||||
|
||||
Args:
|
||||
timedelay (int or float): The delay in seconds
|
||||
|
|
@ -1040,15 +1040,26 @@ def delay(timedelay, callback, *args, **kwargs):
|
|||
commandhandler callback chain, the callback chain can be
|
||||
defined directly in the command body and don't need to be
|
||||
specified here.
|
||||
Reference twisted.internet.defer.Deferred
|
||||
if persistent kwarg is truthy:
|
||||
task_id (int): the task's id intended for use with
|
||||
evennia.scripts.taskhandler.TASK_HANDLER's do_task and remove methods.
|
||||
|
||||
Note:
|
||||
The task handler (`evennia.scripts.taskhandler.TASK_HANDLER`) will
|
||||
be called for persistent or non-persistent tasks.
|
||||
If persistent is set to True, the callback, its arguments
|
||||
and other keyword arguments will be saved in the database,
|
||||
and other keyword arguments will be saved (serialized) in the database,
|
||||
assuming they can be. The callback will be executed even after
|
||||
a server restart/reload, taking into account the specified delay
|
||||
(and server down time).
|
||||
Keep in mind that persistent tasks arguments and callback should not
|
||||
use memory references.
|
||||
If persistent is set to True the delay function will return an int
|
||||
which is the task's id itended for use with TASK_HANDLER's do_task
|
||||
and remove methods.
|
||||
|
||||
All task's whose time delays have passed will be called on server startup.
|
||||
|
||||
"""
|
||||
global _TASK_HANDLER
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue