From 8023adddef1b4724ce717bb5ba8518a102ee1387 Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Fri, 28 May 2021 15:46:37 -0400 Subject: [PATCH 1/3] CmdTasks Created Created a system command for displaying and manipulating tasks. A single unit test in evennia.commands.default.tests.TestHelp fails. Message pattern mismatch. All other unit tests pass. --- evennia/commands/default/cmdset_character.py | 1 + evennia/commands/default/system.py | 184 ++++++++++++++++++- evennia/commands/default/tests.py | 103 +++++++++++ 3 files changed, 287 insertions(+), 1 deletion(-) diff --git a/evennia/commands/default/cmdset_character.py b/evennia/commands/default/cmdset_character.py index 28c8fdd641..dc3b2b58b4 100644 --- a/evennia/commands/default/cmdset_character.py +++ b/evennia/commands/default/cmdset_character.py @@ -50,6 +50,7 @@ class CharacterCmdSet(CmdSet): self.add(system.CmdServerLoad()) # self.add(system.CmdPs()) self.add(system.CmdTickers()) + self.add(system.CmdTasks()) # Admin commands self.add(admin.CmdBoot()) diff --git a/evennia/commands/default/system.py b/evennia/commands/default/system.py index 54db99093e..146e59bc61 100644 --- a/evennia/commands/default/system.py +++ b/evennia/commands/default/system.py @@ -8,7 +8,6 @@ System commands import code import traceback import os -import io import datetime import sys import django @@ -26,8 +25,10 @@ from evennia.utils.eveditor import EvEditor from evennia.utils.evtable import EvTable from evennia.utils.evmore import EvMore from evennia.utils.utils import crop, class_from_module +from evennia.scripts.taskhandler import TaskHandlerTask COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) +_TASK_HANDLER = None # delayed imports _RESOURCE = None @@ -45,6 +46,7 @@ __all__ = ( "CmdAbout", "CmdTime", "CmdServerLoad", + "CmdTasks", ) @@ -1203,3 +1205,183 @@ class CmdTickers(COMMAND_DEFAULT_CLASS): "*" if sub[5] else "-", ) self.caller.msg("|wActive tickers|n:\n" + str(table)) + + +class CmdTasks(COMMAND_DEFAULT_CLASS): + """ + Manage active tasks (delays). + + Note: + Only the action requested on the first switch will be processed. All other switches will + be ignored. + + Manipulation of a single task is intended to be done via the clickable links or through + code directly. Due to generally short life of a task, the inclusion of completion date + and function's memory reference guarentees an incorrect task will not be manipulated. + + By default, tasks that are canceled and never called are automatically removed after + one minute. + + Usage: + tasks + show all current active taks. + tasks[/switch] [function name] + Process the action in the switch to all tasks that are deferring a specific function name. + This would match the name of the callback you passed to a delay or the task handler. + tasks[/switch] [task id], [completion date], [function memory reference] + Process the action in the switch to a specific task. + + Example: + tasks/cancel move_callback + Cancel all movement delays from the slow_exit contrib. + In this example slow exits creates it's tasks with: utils.delay(move_delay, move_callback) + + Switches: + 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. + """ + + key = "tasks" + aliases = ["delays"] + switch_options = ("pause", "unpause", "do_task", "call", "remove", "cancel") + locks = "perm(Developer)" + help_category = "System" + + @staticmethod + def coll_date_func(task): + """Replace regex characters in date string and collect deferred function name.""" + t_comp_date = str(task[0]).replace('-', '/').replace('.', ' ms:') + t_func_name = str(task[1]).split(' ') + t_func_mem_ref = t_func_name[3] if len(t_func_name) >= 4 else None + return t_comp_date, t_func_mem_ref + + def func(self): + caller = self.caller + # get a reference of the global task handler + global _TASK_HANDLER + if _TASK_HANDLER is None: + from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER + # handle no tasks active. + if not _TASK_HANDLER.tasks: + self.msg('There are no active tasks.') + if self.switches or self.args: + self.msg('Likely the task has completed and been removed.') + return + # handle caller's request to manipulate a task(s) + if self.switches or self.lhs: + action_request = self.switches[0] + # handle caller requesting an action on specific deferred function + if len(self.lhslist) == 1: + name_match_found = False + arg_func_name = self.lhslist[0].lower() + # repack tasks into a new dictionary + current_tasks = {} + for task_id, task_args in _TASK_HANDLER.tasks.items(): + current_tasks.update({task_id: task_args}) + # call requested action on all tasks with the function name + for task_id, task_args in current_tasks.items(): + t_func_name = str(task_args[1]).split(' ') + t_func_name = t_func_name[1] if len(t_func_name) >= 2 else None + # skip this task if it is not for the function desired + if arg_func_name != t_func_name: + continue + name_match_found = True + task = TaskHandlerTask(task_id) + switch_action = getattr(task, action_request, False) + if switch_action: + action_return = switch_action() + self.msg(f'Task action {action_request} completed on task ID {task_id}.') + self.msg(f'The task function {action_request} returned: {action_return}') + # provide a message if not tasks of the function name was found + if not name_match_found: + self.msg(f'No tasks deferring function name {arg_func_name} found.') + return + return True + err_arg_msg = 'Task ID, completion date and memory reference are required when manipulating a delay.' + task_comp_msg = 'Task completed while processing request.' + # handle missing arguments or switches + if not self.switches and self.lhs: + self.msg(err_arg_msg) + return + # handle incorrect arguments + if len(self.lhslist) < 2: + self.msg(err_arg_msg) + return + # create a handle for the task + task_id = int(self.lhslist[0]) + task = TaskHandlerTask(task_id) + # handle task no longer existing + if not task.exists(): + self.msg(f'Task {task_id} does not exist.') + return + # get a reference of the function caller requested + switch_action = getattr(task, action_request, False) + if not switch_action: + self.msg(f'{self.switches[0]}, is not an acceptable task action or ' \ + f'{task_comp_msg.lower()}') + # verify manipulating the correct task + if task_id in _TASK_HANDLER.tasks: + task_args = _TASK_HANDLER.tasks.get(task_id, False) + if task_args: # check if the task is still active + sw_comp_date = self.lhslist[1] + sw_func_mem_ref = self.lhslist[2] + t_comp_date, t_func_mem_ref = self.coll_date_func(task_args) + # handle completion date mismatch + if not t_comp_date == sw_comp_date: + self.msg('Task completion time does not match time passed.') + self.msg(task_comp_msg) + self.msg('Likely a new task with the same ID was created') + return + # handle function memory reference mismatch + if not t_func_mem_ref == sw_func_mem_ref: + self.msg("Memory reference for the task's function does not match argument") + self.msg(task_comp_msg) + self.msg('Likely a new task with the same ID was created') + return + else: # task no longer exists + self.msg(task_comp_msg) + return + if task.exists(): # make certain the task has not been called yet. + # call the task's method + action_return = switch_action() + self.msg(f'Task action {action_request} completed.') + self.msg(f'The task function {action_request} returned: {action_return}') + return True + else: + self.msg(task_comp_msg) + return + # No task manupilation requested, build a table of tasks and display it + # get the width of screen in characters + def_screen_width = settings.CLIENT_DEFAULT_WIDTH if settings.CLIENT_DEFAULT_WIDTH else 80 + caller_session = caller.sessions.get() + width = caller_session[0].protocol_flags.get("SCREENWIDTH", def_screen_width) + # create table header and list to hold tasks data and actions + tasks_header = ('Task ID', 'Completion Date', 'Function', 'Arguments', 'KWARGS', 'persistent') + tasks_list = [list() for i in range(len(tasks_header))] # empty list of lists, the size of the header + for task_id, task in _TASK_HANDLER.tasks.items(): + # collect data from the task + t_comp_date, t_func_mem_ref = self.coll_date_func(task) + t_func_name = str(task[1]).split(' ') + t_func_name = t_func_name[1] if len(t_func_name) >= 2 else None + t_args = str(task[2]) + t_kwargs = str(task[3]) + t_pers = str(task[4]) + # add task data to the tasks list + task_data = (task_id, t_comp_date, t_func_name, t_args, t_kwargs, t_pers) + for i in range(len(tasks_header)): + tasks_list[i].append(task_data[i]) + # add task actions to the tasks list + actions = ('pause', 'unpause', 'do_task', 'remove', 'call', 'cancel') + for i in range(len(tasks_header)): + tasks_list[i].append(f"|lc{self.key}/{actions[i]} {task_id}, {t_comp_date}, " \ + f"{t_func_mem_ref}|lt{actions[i]}|le") + # if the screen width is large enough, add directional arrows + if width >= 75: + tasks_list[i][-1] = f"^{tasks_list[i][-1]}^" + # create and display the table + tasks_table = EvTable(*tasks_header, table=tasks_list, maxwidth=width, border='cells', align='center') + self.msg(tasks_table) diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index c086916aca..815ac7ee26 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -18,6 +18,7 @@ from anything import Anything from parameterized import parameterized from django.conf import settings +from twisted.internet import task from unittest.mock import patch, Mock, MagicMock from evennia import DefaultRoom, DefaultExit, ObjectDB @@ -574,6 +575,108 @@ class TestSystem(CommandTest): def test_server_load(self): self.call(system.CmdServerLoad(), "", "Server CPU and Memory load:") +_TASK_HANDLER = None + +def func_test_cmd_tasks(): + return 'success' + +class TestCmdTasks(CommandTest): + + 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.task_handler = _TASK_HANDLER + self.task_handler.clear() + self.task = self.task_handler.add(self.timedelay, func_test_cmd_tasks) + task_args = self.task_handler.tasks.get(self.task.get_id(), False) + self.t_comp_date, self.t_func_mem_ref = system.CmdTasks.coll_date_func(task_args) + + + def tearDown(self): + super().tearDown() + self.task_handler.clear() + + def test_no_tasks(self): + self.task_handler.clear() + self.call(system.CmdTasks(), '', 'There are no active tasks.') + + def test_active_task(self): + cmd_result = self.call(system.CmdTasks(), '') + for ptrn in ('Task ID', 'Completion Date', 'Function', 'KWARGS', 'persisten', + '1', r'\d+/\d+/\d+', r'\d+\:\d+\:\d+', r'ms\:\d+', 'func_test', '{}', + 'False'): + self.assertRegex(cmd_result, ptrn) + + def test_persistent_task(self): + self.task_handler.clear() + self.task_handler.add(self.timedelay, func_test_cmd_tasks, persistent=True) + cmd_result = self.call(system.CmdTasks(), '') + self.assertRegex(cmd_result, 'True') + + def test_pause_unpause(self): + # test pause + args = f'/pause {self.task.get_id()}, {self.t_comp_date}, {self.t_func_mem_ref}' + wanted_msg = 'Task action pause completed.|The task function pause returned:' + self.call(system.CmdTasks(), args, wanted_msg) + self.assertTrue(self.task.paused) + self.task_handler.clock.advance(self.timedelay + 1) + # test unpause + args = f'/unpause {self.task.get_id()}, {self.t_comp_date}, {self.t_func_mem_ref}' + self.assertTrue(self.task.exists()) + wanted_msg = 'Task action unpause completed.|The task function unpause returned: None' + self.call(system.CmdTasks(), args, wanted_msg) + # verify task continues after unpause + self.task_handler.clock.advance(1) + self.assertFalse(self.task.exists()) + + def test_do_task(self): + args = f'/do_task {self.task.get_id()}, {self.t_comp_date}, {self.t_func_mem_ref}' + wanted_msg = 'Task action do_task completed.|The task function do_task returned: success' + self.call(system.CmdTasks(), args, wanted_msg) + self.assertFalse(self.task.exists()) + + def test_remove(self): + args = f'/remove {self.task.get_id()}, {self.t_comp_date}, {self.t_func_mem_ref}' + wanted_msg = 'Task action remove completed.|The task function remove returned: True' + self.call(system.CmdTasks(), args, wanted_msg) + self.assertFalse(self.task.exists()) + + def test_call(self): + args = f'/call {self.task.get_id()}, {self.t_comp_date}, {self.t_func_mem_ref}' + wanted_msg = 'Task action call completed.|The task function call returned: success' + self.call(system.CmdTasks(), args, wanted_msg) + # make certain the task is still active + self.assertTrue(self.task.active()) + # go past delay time, the task should call do_task and remove itself after calling. + self.task_handler.clock.advance(self.timedelay + 1) + self.assertFalse(self.task.exists()) + + def test_cancel(self): + args = f'/cancel {self.task.get_id()}, {self.t_comp_date}, {self.t_func_mem_ref}' + wanted_msg = 'Task action cancel completed.|The task function cancel returned: True' + self.call(system.CmdTasks(), args, wanted_msg) + self.assertTrue(self.task.exists()) + self.assertFalse(self.task.active()) + + def test_func_name_manipulation(self): + self.task_handler.add(self.timedelay, func_test_cmd_tasks) # add an extra task + args = f'/remove func_test_cmd_tasks' + wanted_msg = 'Task action remove completed on task ID 1.|The task function remove returned: True|' \ + 'Task action remove completed on task ID 2.|The task function remove returned: True' + self.call(system.CmdTasks(), args, wanted_msg) + self.assertFalse(self.task_handler.tasks) # no tasks should exist. + + def test_wrong_func_name(self): + args = f'/remove intentional_fail' + wanted_msg = 'No tasks deferring function name intentional_fail found.' + self.call(system.CmdTasks(), args, wanted_msg) + self.assertTrue(self.task.active()) + class TestAdmin(CommandTest): def test_emit(self): From 03b0bc47a059cc31e96193d30d82255c04c54f9f Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Mon, 31 May 2021 20:04:13 -0400 Subject: [PATCH 2/3] CmdTask Misc Updates --- evennia/commands/default/system.py | 85 +++++++++++++++++------------- 1 file changed, 47 insertions(+), 38 deletions(-) diff --git a/evennia/commands/default/system.py b/evennia/commands/default/system.py index 146e59bc61..9bb1f05b40 100644 --- a/evennia/commands/default/system.py +++ b/evennia/commands/default/system.py @@ -1209,40 +1209,46 @@ class CmdTickers(COMMAND_DEFAULT_CLASS): class CmdTasks(COMMAND_DEFAULT_CLASS): """ - Manage active tasks (delays). - - Note: - Only the action requested on the first switch will be processed. All other switches will - be ignored. - - Manipulation of a single task is intended to be done via the clickable links or through - code directly. Due to generally short life of a task, the inclusion of completion date - and function's memory reference guarentees an incorrect task will not be manipulated. - - By default, tasks that are canceled and never called are automatically removed after - one minute. + Display or terminate active tasks (delays). Usage: - tasks - show all current active taks. - tasks[/switch] [function name] - Process the action in the switch to all tasks that are deferring a specific function name. - This would match the name of the callback you passed to a delay or the task handler. - tasks[/switch] [task id], [completion date], [function memory reference] - Process the action in the switch to a specific task. - - Example: - tasks/cancel move_callback - Cancel all movement delays from the slow_exit contrib. - In this example slow exits creates it's tasks with: utils.delay(move_delay, move_callback) + tasks + show all current active taks. + tasks[/switch] [function name] + Process the action in the switch to all tasks that are deferring a specific function name. + This would match the name of the callback you passed to a delay or the task handler. + tasks[/switch] [task id], [completion date], [function memory reference] + Process the action in the switch to a specific task. Switches: - 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. + 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. + + Notes: + A task is a single use method of delaying the call of a function. + + Tasks can be created using utils.delay. + Reference: https://evennia.readthedocs.io/en/latest/Command-Duration.html + + Only the action requested on the first switch will be processed. All other switches will + be ignored. + + Manipulation of a single task is intended to be done via the clickable links or through + code directly. Due to generally short life of a task, the inclusion of completion date + and function's memory reference guarentees an incorrect task will not be manipulated. + + By default, tasks that are canceled and never called are automatically removed after + one minute. + + Example: + tasks/cancel move_callback + Cancel all movement delays from the slow_exit contrib. + In this example slow exits creates it's tasks with: utils.delay(move_delay, move_callback) + """ key = "tasks" @@ -1260,7 +1266,6 @@ class CmdTasks(COMMAND_DEFAULT_CLASS): return t_comp_date, t_func_mem_ref def func(self): - caller = self.caller # get a reference of the global task handler global _TASK_HANDLER if _TASK_HANDLER is None: @@ -1271,6 +1276,7 @@ class CmdTasks(COMMAND_DEFAULT_CLASS): if self.switches or self.args: self.msg('Likely the task has completed and been removed.') return + # handle caller's request to manipulate a task(s) if self.switches or self.lhs: action_request = self.switches[0] @@ -1301,7 +1307,8 @@ class CmdTasks(COMMAND_DEFAULT_CLASS): self.msg(f'No tasks deferring function name {arg_func_name} found.') return return True - err_arg_msg = 'Task ID, completion date and memory reference are required when manipulating a delay.' + err_arg_msg = 'Task ID, completion date and memory reference are required when ' \ + 'manipulating a delay.' task_comp_msg = 'Task completed while processing request.' # handle missing arguments or switches if not self.switches and self.lhs: @@ -1354,14 +1361,15 @@ class CmdTasks(COMMAND_DEFAULT_CLASS): else: self.msg(task_comp_msg) return + # No task manupilation requested, build a table of tasks and display it # get the width of screen in characters - def_screen_width = settings.CLIENT_DEFAULT_WIDTH if settings.CLIENT_DEFAULT_WIDTH else 80 - caller_session = caller.sessions.get() - width = caller_session[0].protocol_flags.get("SCREENWIDTH", def_screen_width) + width = self.client_width() # create table header and list to hold tasks data and actions - tasks_header = ('Task ID', 'Completion Date', 'Function', 'Arguments', 'KWARGS', 'persistent') - tasks_list = [list() for i in range(len(tasks_header))] # empty list of lists, the size of the header + tasks_header = ('Task ID', 'Completion Date', 'Function', 'Arguments', 'KWARGS', + 'persistent') + # empty list of lists, the size of the header + tasks_list = [list() for i in range(len(tasks_header))] for task_id, task in _TASK_HANDLER.tasks.items(): # collect data from the task t_comp_date, t_func_mem_ref = self.coll_date_func(task) @@ -1383,5 +1391,6 @@ class CmdTasks(COMMAND_DEFAULT_CLASS): if width >= 75: tasks_list[i][-1] = f"^{tasks_list[i][-1]}^" # create and display the table - tasks_table = EvTable(*tasks_header, table=tasks_list, maxwidth=width, border='cells', align='center') + tasks_table = EvTable(*tasks_header, table=tasks_list, maxwidth=width, border='cells', + align='center') self.msg(tasks_table) From 93b68e3a312ba6f53d2ff9fa52fa91e74bfade16 Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Mon, 14 Jun 2021 18:43:18 -0400 Subject: [PATCH 3/3] CmdTasks removed text tags added yes no --- evennia/commands/default/system.py | 183 +++++++++++++++++------------ evennia/commands/default/tests.py | 100 +++++++++++++--- 2 files changed, 192 insertions(+), 91 deletions(-) diff --git a/evennia/commands/default/system.py b/evennia/commands/default/system.py index 9bb1f05b40..88e8421152 100644 --- a/evennia/commands/default/system.py +++ b/evennia/commands/default/system.py @@ -24,6 +24,7 @@ from evennia.utils import logger, utils, gametime, create, search from evennia.utils.eveditor import EvEditor from evennia.utils.evtable import EvTable from evennia.utils.evmore import EvMore +from evennia.utils.evmenu import ask_yes_no from evennia.utils.utils import crop, class_from_module from evennia.scripts.taskhandler import TaskHandlerTask @@ -1217,7 +1218,7 @@ class CmdTasks(COMMAND_DEFAULT_CLASS): tasks[/switch] [function name] Process the action in the switch to all tasks that are deferring a specific function name. This would match the name of the callback you passed to a delay or the task handler. - tasks[/switch] [task id], [completion date], [function memory reference] + tasks[/switch] [task id] Process the action in the switch to a specific task. Switches: @@ -1237,17 +1238,16 @@ class CmdTasks(COMMAND_DEFAULT_CLASS): Only the action requested on the first switch will be processed. All other switches will be ignored. - Manipulation of a single task is intended to be done via the clickable links or through - code directly. Due to generally short life of a task, the inclusion of completion date - and function's memory reference guarentees an incorrect task will not be manipulated. - By default, tasks that are canceled and never called are automatically removed after one minute. Example: tasks/cancel move_callback - Cancel all movement delays from the slow_exit contrib. - In this example slow exits creates it's tasks with: utils.delay(move_delay, move_callback) + Cancel all movement delays from the slow_exit contrib. + In this example slow exits creates it's tasks with: + utils.delay(move_delay, move_callback) + tasks/cancel 2 + Cancel task id 2. """ @@ -1265,6 +1265,36 @@ class CmdTasks(COMMAND_DEFAULT_CLASS): t_func_mem_ref = t_func_name[3] if len(t_func_name) >= 4 else None return t_comp_date, t_func_mem_ref + def do_task_action(self, *args, **kwargs): + """ + Process the action of a tasks command. + + This exists to gain support with yes or no function from EvMenu. + """ + task_id = self.task_id + + # get a reference of the global task handler + global _TASK_HANDLER + if _TASK_HANDLER is None: + from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER + + # verify manipulating the correct task + task_args = _TASK_HANDLER.tasks.get(task_id, False) + if not task_args: # check if the task is still active + self.msg('Task completed while waiting for input.') + return + else: + # make certain a task with matching IDs has not been created + t_comp_date, t_func_mem_ref = self.coll_date_func(task_args) + if self.t_comp_date != t_comp_date or self.t_func_mem_ref != t_func_mem_ref: + self.msg('Task completed while waiting for input.') + return + + # Do the action requested by command caller + action_return = self.task_action() + self.msg(f'{self.action_request} request completed.') + self.msg(f'The task function {self.action_request} returned: {action_return}') + def func(self): # get a reference of the global task handler global _TASK_HANDLER @@ -1278,16 +1308,80 @@ class CmdTasks(COMMAND_DEFAULT_CLASS): return # handle caller's request to manipulate a task(s) - if self.switches or self.lhs: + if self.switches and self.lhs: + + # find if the argument is a task id or function name action_request = self.switches[0] - # handle caller requesting an action on specific deferred function - if len(self.lhslist) == 1: + try: + arg_is_id = int(self.lhslist[0]) + except ValueError: + arg_is_id = False + + # if the argument is a task id, proccess the action on a single task + if arg_is_id: + + err_arg_msg = 'Switch and task ID are required when manipulating a task.' + task_comp_msg = 'Task completed while processing request.' + + # handle missing arguments or switches + if not self.switches and self.lhs: + self.msg(err_arg_msg) + return + + # create a handle for the task + task_id = arg_is_id + task = TaskHandlerTask(task_id) + + # handle task no longer existing + if not task.exists(): + self.msg(f'Task {task_id} does not exist.') + return + + # get a reference of the function caller requested + switch_action = getattr(task, action_request, False) + if not switch_action: + self.msg(f'{self.switches[0]}, is not an acceptable task action or ' \ + f'{task_comp_msg.lower()}') + + # verify manipulating the correct task + if task_id in _TASK_HANDLER.tasks: + task_args = _TASK_HANDLER.tasks.get(task_id, False) + if not task_args: # check if the task is still active + self.msg(task_comp_msg) + return + else: + t_comp_date, t_func_mem_ref = self.coll_date_func(task_args) + t_func_name = str(task_args[1]).split(' ') + t_func_name = t_func_name[1] if len(t_func_name) >= 2 else None + + if task.exists(): # make certain the task has not been called yet. + prompt = f'Yes or No, {action_request} task {task_id}? With completion date ' \ + f'{t_comp_date}. Deferring function {t_func_name}.' + no_msg = f'No {action_request} processed.' + # record variables for use in do_task_action method + self.task_id = task_id + self.t_comp_date = t_comp_date + self.t_func_mem_ref = t_func_mem_ref + self.task_action = switch_action + self.action_request = action_request + ask_yes_no(self.caller, prompt, self.do_task_action, no_msg, no_msg, True) + return True + else: + self.msg(task_comp_msg) + return + + # the argument is not a task id, process the action on all task deferring the function + # specified as an argument + else: + name_match_found = False arg_func_name = self.lhslist[0].lower() + # repack tasks into a new dictionary current_tasks = {} for task_id, task_args in _TASK_HANDLER.tasks.items(): current_tasks.update({task_id: task_args}) + # call requested action on all tasks with the function name for task_id, task_args in current_tasks.items(): t_func_name = str(task_args[1]).split(' ') @@ -1302,65 +1396,18 @@ class CmdTasks(COMMAND_DEFAULT_CLASS): action_return = switch_action() self.msg(f'Task action {action_request} completed on task ID {task_id}.') self.msg(f'The task function {action_request} returned: {action_return}') + # provide a message if not tasks of the function name was found if not name_match_found: self.msg(f'No tasks deferring function name {arg_func_name} found.') return return True - err_arg_msg = 'Task ID, completion date and memory reference are required when ' \ - 'manipulating a delay.' - task_comp_msg = 'Task completed while processing request.' - # handle missing arguments or switches - if not self.switches and self.lhs: - self.msg(err_arg_msg) - return - # handle incorrect arguments - if len(self.lhslist) < 2: - self.msg(err_arg_msg) - return - # create a handle for the task - task_id = int(self.lhslist[0]) - task = TaskHandlerTask(task_id) - # handle task no longer existing - if not task.exists(): - self.msg(f'Task {task_id} does not exist.') - return - # get a reference of the function caller requested - switch_action = getattr(task, action_request, False) - if not switch_action: - self.msg(f'{self.switches[0]}, is not an acceptable task action or ' \ - f'{task_comp_msg.lower()}') - # verify manipulating the correct task - if task_id in _TASK_HANDLER.tasks: - task_args = _TASK_HANDLER.tasks.get(task_id, False) - if task_args: # check if the task is still active - sw_comp_date = self.lhslist[1] - sw_func_mem_ref = self.lhslist[2] - t_comp_date, t_func_mem_ref = self.coll_date_func(task_args) - # handle completion date mismatch - if not t_comp_date == sw_comp_date: - self.msg('Task completion time does not match time passed.') - self.msg(task_comp_msg) - self.msg('Likely a new task with the same ID was created') - return - # handle function memory reference mismatch - if not t_func_mem_ref == sw_func_mem_ref: - self.msg("Memory reference for the task's function does not match argument") - self.msg(task_comp_msg) - self.msg('Likely a new task with the same ID was created') - return - else: # task no longer exists - self.msg(task_comp_msg) - return - if task.exists(): # make certain the task has not been called yet. - # call the task's method - action_return = switch_action() - self.msg(f'Task action {action_request} completed.') - self.msg(f'The task function {action_request} returned: {action_return}') - return True - else: - self.msg(task_comp_msg) - return + + # check if an maleformed request was created + elif self.switches or self.lhs: + self.msg('Task command misformed.') + self.msg('Proper format tasks[/switch] [function name or task id]') + return # No task manupilation requested, build a table of tasks and display it # get the width of screen in characters @@ -1382,14 +1429,6 @@ class CmdTasks(COMMAND_DEFAULT_CLASS): task_data = (task_id, t_comp_date, t_func_name, t_args, t_kwargs, t_pers) for i in range(len(tasks_header)): tasks_list[i].append(task_data[i]) - # add task actions to the tasks list - actions = ('pause', 'unpause', 'do_task', 'remove', 'call', 'cancel') - for i in range(len(tasks_header)): - tasks_list[i].append(f"|lc{self.key}/{actions[i]} {task_id}, {t_comp_date}, " \ - f"{t_func_mem_ref}|lt{actions[i]}|le") - # if the screen width is large enough, add directional arrows - if width >= 75: - tasks_list[i][-1] = f"^{tasks_list[i][-1]}^" # create and display the table tasks_table = EvTable(*tasks_header, table=tasks_list, maxwidth=width, border='cells', align='center') diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 815ac7ee26..f7d70d059f 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -594,7 +594,6 @@ class TestCmdTasks(CommandTest): self.task_handler.clear() self.task = self.task_handler.add(self.timedelay, func_test_cmd_tasks) task_args = self.task_handler.tasks.get(self.task.get_id(), False) - self.t_comp_date, self.t_func_mem_ref = system.CmdTasks.coll_date_func(task_args) def tearDown(self): @@ -620,36 +619,46 @@ class TestCmdTasks(CommandTest): def test_pause_unpause(self): # test pause - args = f'/pause {self.task.get_id()}, {self.t_comp_date}, {self.t_func_mem_ref}' - wanted_msg = 'Task action pause completed.|The task function pause returned:' - self.call(system.CmdTasks(), args, wanted_msg) + args = f'/pause {self.task.get_id()}' + wanted_msg = 'Yes or No, pause task 1? With completion date' + cmd_result = self.call(system.CmdTasks(), args, wanted_msg) + self.assertRegex(cmd_result, '\. Deferring function func_test_cmd_tasks\.') + self.char1.execute_cmd('y') self.assertTrue(self.task.paused) self.task_handler.clock.advance(self.timedelay + 1) # test unpause - args = f'/unpause {self.task.get_id()}, {self.t_comp_date}, {self.t_func_mem_ref}' + args = f'/unpause {self.task.get_id()}' self.assertTrue(self.task.exists()) - wanted_msg = 'Task action unpause completed.|The task function unpause returned: None' - self.call(system.CmdTasks(), args, wanted_msg) + wanted_msg = 'Yes or No, unpause task 1? With completion date' + cmd_result = self.call(system.CmdTasks(), args, wanted_msg) + self.assertRegex(cmd_result, '\. Deferring function func_test_cmd_tasks\.') + self.char1.execute_cmd('y') # verify task continues after unpause self.task_handler.clock.advance(1) self.assertFalse(self.task.exists()) def test_do_task(self): - args = f'/do_task {self.task.get_id()}, {self.t_comp_date}, {self.t_func_mem_ref}' - wanted_msg = 'Task action do_task completed.|The task function do_task returned: success' - self.call(system.CmdTasks(), args, wanted_msg) + args = f'/do_task {self.task.get_id()}' + wanted_msg = 'Yes or No, do_task task 1? With completion date' + cmd_result = self.call(system.CmdTasks(), args, wanted_msg) + self.assertRegex(cmd_result, '\. Deferring function func_test_cmd_tasks\.') + self.char1.execute_cmd('y') self.assertFalse(self.task.exists()) def test_remove(self): - args = f'/remove {self.task.get_id()}, {self.t_comp_date}, {self.t_func_mem_ref}' - wanted_msg = 'Task action remove completed.|The task function remove returned: True' - self.call(system.CmdTasks(), args, wanted_msg) + args = f'/remove {self.task.get_id()}' + wanted_msg = 'Yes or No, remove task 1? With completion date' + cmd_result = self.call(system.CmdTasks(), args, wanted_msg) + self.assertRegex(cmd_result, '\. Deferring function func_test_cmd_tasks\.') + self.char1.execute_cmd('y') self.assertFalse(self.task.exists()) def test_call(self): - args = f'/call {self.task.get_id()}, {self.t_comp_date}, {self.t_func_mem_ref}' - wanted_msg = 'Task action call completed.|The task function call returned: success' - self.call(system.CmdTasks(), args, wanted_msg) + args = f'/call {self.task.get_id()}' + wanted_msg = 'Yes or No, call task 1? With completion date' + cmd_result = self.call(system.CmdTasks(), args, wanted_msg) + self.assertRegex(cmd_result, '\. Deferring function func_test_cmd_tasks\.') + self.char1.execute_cmd('y') # make certain the task is still active self.assertTrue(self.task.active()) # go past delay time, the task should call do_task and remove itself after calling. @@ -657,9 +666,11 @@ class TestCmdTasks(CommandTest): self.assertFalse(self.task.exists()) def test_cancel(self): - args = f'/cancel {self.task.get_id()}, {self.t_comp_date}, {self.t_func_mem_ref}' - wanted_msg = 'Task action cancel completed.|The task function cancel returned: True' - self.call(system.CmdTasks(), args, wanted_msg) + args = f'/cancel {self.task.get_id()}' + wanted_msg = 'Yes or No, cancel task 1? With completion date' + cmd_result = self.call(system.CmdTasks(), args, wanted_msg) + self.assertRegex(cmd_result, '\. Deferring function func_test_cmd_tasks\.') + self.char1.execute_cmd('y') self.assertTrue(self.task.exists()) self.assertFalse(self.task.active()) @@ -677,6 +688,57 @@ class TestCmdTasks(CommandTest): self.call(system.CmdTasks(), args, wanted_msg) self.assertTrue(self.task.active()) + def test_no_input(self): + args = f'/cancel {self.task.get_id()}' + self.call(system.CmdTasks(), args) + # task should complete since no input was received + self.task_handler.clock.advance(self.timedelay + 1) + self.assertFalse(self.task.exists()) + + def test_responce_of_yes(self): + self.call(system.CmdTasks(), f'/cancel {self.task.get_id()}') + self.char1.msg = Mock() + self.char1.execute_cmd('y') + text = '' + for _, _, kwargs in self.char1.msg.mock_calls: + text += kwargs.get('text', '') + self.assertEqual(text, 'cancel request completed.The task function cancel returned: True') + self.assertTrue(self.task.exists()) + + def test_task_complete_waiting_input(self): + """Test for task completing while waiting for input.""" + self.call(system.CmdTasks(), f'/cancel {self.task.get_id()}') + self.task_handler.clock.advance(self.timedelay + 1) + self.char1.msg = Mock() + self.char1.execute_cmd('y') + text = '' + for _, _, kwargs in self.char1.msg.mock_calls: + text += kwargs.get('text', '') + self.assertEqual(text, 'Task completed while waiting for input.') + self.assertFalse(self.task.exists()) + + def test_new_task_waiting_input(self): + """ + Test task completing than a new task with the same ID being made while waitinf for input. + """ + self.assertTrue(self.task.get_id(), 1) + self.call(system.CmdTasks(), f'/cancel {self.task.get_id()}') + self.task_handler.clock.advance(self.timedelay + 1) + self.assertFalse(self.task.exists()) + self.task = self.task_handler.add(self.timedelay, func_test_cmd_tasks) + self.assertTrue(self.task.get_id(), 1) + self.char1.msg = Mock() + self.char1.execute_cmd('y') + text = '' + for _, _, kwargs in self.char1.msg.mock_calls: + text += kwargs.get('text', '') + self.assertEqual(text, 'Task completed while waiting for input.') + + def test_misformed_command(self): + wanted_msg = 'Task command misformed.|Proper format tasks[/switch] ' \ + '[function name or task id]' + self.call(system.CmdTasks(), f'/cancel', wanted_msg) + class TestAdmin(CommandTest): def test_emit(self):