mirror of
https://github.com/evennia/evennia.git
synced 2026-03-21 23:36:30 +01:00
Merge branch 'develop' into mapping-contrib
This commit is contained in:
commit
79019a5813
16 changed files with 503 additions and 81 deletions
|
|
@ -66,6 +66,9 @@ Up requirements to Django 3.2+
|
|||
into a more consistent structure for overriding. Expanded webpage documentation considerably.
|
||||
- REST API list-view was shortened (#2401). New CSS/HTML. Add ReDoc for API autodoc page.
|
||||
- Update and fix dummyrunner with cleaner code and setup.
|
||||
- Made `iter_to_str` format prettier strings, using Oxford comma.
|
||||
- Added an MXP anchor tag to also support clickable web links.
|
||||
- New `tasks` command for managing tasks started with `utils.delay` (PR by davewiththenicehat)
|
||||
|
||||
### Evennia 0.9.5 (2019-2020)
|
||||
|
||||
|
|
@ -149,6 +152,7 @@ without arguments starts a full interactive Python console.
|
|||
- Include more Web-client info in `session.protocol_flags`.
|
||||
- Fixes in multi-match situations - don't allow finding/listing multimatches for 3-box when
|
||||
only two boxes in location.
|
||||
- Fix for TaskHandler with proper deferred returns/ability to cancel etc (PR by davewiththenicehat)
|
||||
|
||||
|
||||
## Evennia 0.9 (2018-2019)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
## Clickable links
|
||||
|
||||
Evennia supports clickable links for clients that supports it. This marks certain text so it can be
|
||||
clicked by a mouse and trigger a given Evennia command. To support clickable links, Evennia requires
|
||||
the webclient or an third-party telnet client with [MXP](http://www.zuggsoft.com/zmud/mxp.htm)
|
||||
support (*Note: Evennia only supports clickable links, no other MXP features*).
|
||||
clicked by a mouse and either trigger a given Evennia command, or open a URL in an external web
|
||||
browser. To support clickable links, Evennia requires the webclient or an third-party telnet client
|
||||
with [MXP](http://www.zuggsoft.com/zmud/mxp.htm) support (*Note: Evennia only supports clickable links, no other MXP features*).
|
||||
|
||||
- `|lc` to start the link, by defining the command to execute.
|
||||
- `|lu` to start the link, by defining the URL to open.
|
||||
- `|lt` to continue with the text to show to the user (the link text).
|
||||
- `|le` to end the link text and the link definition.
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@
|
|||
### How to *get* Help
|
||||
|
||||
If you cannot find what you are looking for in the documentation, here's what to do:
|
||||
|
||||
|
||||
- If you think the documentation is not clear enough, create a [documentation ticket](github:issue).
|
||||
- If you have trouble with a missing feature or a problem you think is a bug, look through the
|
||||
the list of known [issue][issues] if you can't find your issue in the list, make a
|
||||
- If you have trouble with a missing feature or a problem you think is a bug, look through the
|
||||
the list of known [issue][issues] if you can't find your issue in the list, make a
|
||||
new one [here](github:issue).
|
||||
- If you need help, want to start a discussion or get some input on something you are working on,
|
||||
make a post to the [discussions group][group] This is technically a 'mailing list', but you don't
|
||||
|
|
@ -26,8 +26,8 @@ eventually!
|
|||
Evennia is open-source and non-commercial. It relies on the time donated by its users and developers in order to progress.
|
||||
|
||||
- Spread the word! If you like Evennia, consider writing a blog post about it.
|
||||
- Take part in the Evennia community! Join the [chat][chat] or [forum][group].
|
||||
- Report problems you find or features you'd like to our [issue tracker](github:issue).
|
||||
- Take part in the Evennia community! Join the [chat][chat] or [forum][group].
|
||||
- Report problems you find or features you'd like to our [issue tracker](github:issue).
|
||||
|
||||
```important::
|
||||
Just the simple act of us know you are out there using Evennia helps a lot!
|
||||
|
|
@ -35,9 +35,9 @@ Evennia is open-source and non-commercial. It relies on the time donated by its
|
|||
|
||||
If you'd like to help develop Evennia more hands-on, here are some ways to get going:
|
||||
|
||||
- Look through this [online documentation](./index#Evennia-documentation) and see if you can help improve or expand the
|
||||
documentation (even small things like fixing typos!). [See here](./Contributing-Docs) on how you
|
||||
contribute to the docs.
|
||||
- Look through this [online documentation](./index#Evennia-documentation) and see if you can help improve or expand the
|
||||
documentation (even small things like fixing typos!). [See here](./Contributing-Docs) on how you
|
||||
contribute to the docs.
|
||||
- Send a message to our [discussion group][group] and/or our [IRC chat][chat] asking about what
|
||||
needs doing, along with what your interests and skills are.
|
||||
- Take a look at our [issue tracker][issues] and see if there's something you feel like taking on.
|
||||
|
|
@ -47,10 +47,10 @@ needs doing, along with what your interests and skills are.
|
|||
github.
|
||||
|
||||
... And finally, if you want to help motivate and support development you can also drop some coins
|
||||
in the developer's cup. You can [make a donation via PayPal][paypal] or, even better, [become an
|
||||
Evennia patron on Patreon][patreon]! This is a great way to tip your hat and show that you
|
||||
in the developer's cup. You can [make a donation via PayPal][paypal] or, even better,
|
||||
[become an Evennia patron on Patreon][patreon]! This is a great way to tip your hat and show that you
|
||||
appreciate the work done with the server! You can also encourage the community to take on particular
|
||||
issues by putting up a monetary [bounty][bountysource] on it.
|
||||
issues by putting up a monetary [bounty][bountysource] on it.
|
||||
|
||||
|
||||
[form]: https://docs.google.com/spreadsheet/viewform?hl=en_US&formkey=dGN0VlJXMWpCT3VHaHpscDEzY1RoZGc6MQ#gid=0
|
||||
|
|
@ -63,4 +63,4 @@ issues by putting up a monetary [bounty][bountysource] on it.
|
|||
[patreon]: https://www.patreon.com/griatch
|
||||
[patreon-img]: http://www.evennia.com/_/rsrc/1424724909023/home/evennia_patreon_100x100.png
|
||||
[issues-bounties]:https://github.com/evennia/evennia/labels/bounty
|
||||
[bountysource]: https://www.bountysource.com/teams/evennia
|
||||
[bountysource]: https://www.bountysource.com/teams/evennia
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ System commands
|
|||
import code
|
||||
import traceback
|
||||
import os
|
||||
import io
|
||||
import datetime
|
||||
import sys
|
||||
import django
|
||||
|
|
@ -25,9 +24,12 @@ 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.utils import crop, class_from_module
|
||||
from evennia.utils.evmenu import ask_yes_no
|
||||
from evennia.utils.utils import crop, class_from_module, iter_to_str
|
||||
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 +47,7 @@ __all__ = (
|
|||
"CmdAbout",
|
||||
"CmdTime",
|
||||
"CmdServerLoad",
|
||||
"CmdTasks",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1203,3 +1206,224 @@ class CmdTickers(COMMAND_DEFAULT_CLASS):
|
|||
"*" if sub[5] else "-",
|
||||
)
|
||||
self.caller.msg("|wActive tickers|n:\n" + str(table))
|
||||
|
||||
|
||||
class CmdTasks(COMMAND_DEFAULT_CLASS):
|
||||
"""
|
||||
Display or terminate active tasks (delays).
|
||||
|
||||
Usage:
|
||||
tasks[/switch] [task_id or function_name]
|
||||
|
||||
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.
|
||||
|
||||
Notes:
|
||||
A task is a single use method of delaying the call of a function. Calls are created
|
||||
in code, using `evennia.utils.delay`.
|
||||
See |luhttps://www.evennia.com/docs/latest/Command-Duration.html|ltthe docs|le for help.
|
||||
|
||||
By default, tasks that are canceled and never called are cleaned up after one minute.
|
||||
|
||||
Examples:
|
||||
- `tasks/cancel move_callback` - Cancels 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.
|
||||
|
||||
"""
|
||||
|
||||
key = "tasks"
|
||||
aliases = ["delays", "task"]
|
||||
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('-', '/')
|
||||
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 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
|
||||
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 and self.lhs:
|
||||
|
||||
# find if the argument is a task id or function name
|
||||
action_request = self.switches[0]
|
||||
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'{action_request.capitalize()} task {task_id} with completion date '
|
||||
f'{t_comp_date} ({t_func_name}) {{options}}?')
|
||||
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=prompt,
|
||||
yes_action=self.do_task_action,
|
||||
no_action=no_msg,
|
||||
default="Y",
|
||||
allow_abort=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(' ')
|
||||
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
|
||||
|
||||
# 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
|
||||
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')
|
||||
# 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)
|
||||
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])
|
||||
# create and display the table
|
||||
tasks_table = EvTable(*tasks_header, table=tasks_list, maxwidth=width, border='cells',
|
||||
align='center')
|
||||
actions = (f'/{switch}' for switch in self.switch_options)
|
||||
helptxt = f"\nActions: {iter_to_str(actions)}"
|
||||
self.msg(str(tasks_table) + helptxt)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -575,6 +576,170 @@ 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)
|
||||
|
||||
|
||||
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()}'
|
||||
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.assertTrue(self.task.exists())
|
||||
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()}'
|
||||
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()}'
|
||||
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()}'
|
||||
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.
|
||||
self.task_handler.clock.advance(self.timedelay + 1)
|
||||
self.assertFalse(self.task.exists())
|
||||
|
||||
def test_cancel(self):
|
||||
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())
|
||||
|
||||
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())
|
||||
|
||||
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):
|
||||
|
|
|
|||
|
|
@ -182,9 +182,9 @@ class TestCraftingRecipe(TestCase):
|
|||
"i1": "cons2",
|
||||
"i2": "cons3",
|
||||
"o0": "Result1",
|
||||
"tools": "bar, bar2 and bar3",
|
||||
"tools": "bar, bar2, and bar3",
|
||||
"consumables": "cons1 and cons2",
|
||||
"inputs": "cons1, cons2 and cons3",
|
||||
"inputs": "cons1, cons2, and cons3",
|
||||
"outputs": "Result1",
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -292,14 +292,25 @@ class TaskHandler(object):
|
|||
if not persistent:
|
||||
continue
|
||||
|
||||
safe_callback = callback
|
||||
if getattr(callback, "__self__", None):
|
||||
# `callback` is an instance method
|
||||
obj = callback.__self__
|
||||
name = callback.__name__
|
||||
callback = (obj, name)
|
||||
safe_callback = (obj, name)
|
||||
|
||||
# Check if callback can be pickled. args and kwargs have been checked
|
||||
self.to_save[task_id] = dbserialize((date, callback, args, kwargs))
|
||||
try:
|
||||
dbserialize(safe_callback)
|
||||
except (TypeError, AttributeError, PickleError) as err:
|
||||
raise ValueError(
|
||||
"the specified callback {callback} cannot be pickled. "
|
||||
"It must be a top-level function in a module or an "
|
||||
"instance method ({err}).".format(callback=callback, err=err)
|
||||
)
|
||||
|
||||
self.to_save[task_id] = dbserialize((date, safe_callback, args, kwargs))
|
||||
|
||||
ServerConfig.objects.conf("delayed_tasks", self.to_save)
|
||||
|
||||
def add(self, timedelay, callback, *args, **kwargs):
|
||||
|
|
@ -348,17 +359,6 @@ class TaskHandler(object):
|
|||
safe_args = []
|
||||
safe_kwargs = {}
|
||||
|
||||
# an unsaveable callback should immediately abort
|
||||
try:
|
||||
dbserialize(callback)
|
||||
except (TypeError, AttributeError, PickleError):
|
||||
raise ValueError(
|
||||
"the specified callback {} cannot be pickled. "
|
||||
"It must be a top-level function in a module or an "
|
||||
"instance method.".format(callback)
|
||||
)
|
||||
return
|
||||
|
||||
# Check that args and kwargs contain picklable information
|
||||
for arg in args:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -16,12 +16,14 @@ http://www.gammon.com.au/mushclient/addingservermxp.htm
|
|||
import re
|
||||
|
||||
LINKS_SUB = re.compile(r"\|lc(.*?)\|lt(.*?)\|le", re.DOTALL)
|
||||
URL_SUB = re.compile(r"\|lu(.*?)\|lt(.*?)\|le", re.DOTALL)
|
||||
|
||||
# MXP Telnet option
|
||||
MXP = bytes([91]) # b"\x5b"
|
||||
|
||||
MXP_TEMPSECURE = "\x1B[4z"
|
||||
MXP_SEND = MXP_TEMPSECURE + '<SEND HREF="\\1">' + "\\2" + MXP_TEMPSECURE + "</SEND>"
|
||||
MXP_URL = MXP_TEMPSECURE + '<A HREF="\\1">' + "\\2" + MXP_TEMPSECURE + "</A>"
|
||||
|
||||
|
||||
def mxp_parse(text):
|
||||
|
|
@ -38,6 +40,7 @@ def mxp_parse(text):
|
|||
text = text.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
|
||||
text = LINKS_SUB.sub(MXP_SEND, text)
|
||||
text = URL_SUB.sub(MXP_URL, text)
|
||||
return text
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -128,8 +128,8 @@ def c_login(client):
|
|||
|
||||
def c_login_nodig(client):
|
||||
"logins, don't dig its own room"
|
||||
cname = DUMMY_NAME % client.gid
|
||||
cpwd = DUMMY_PWD % client.gid
|
||||
cname = DUMMY_NAME.format(gid=client.gid)
|
||||
cpwd = DUMMY_PWD.format(gid=client.gid)
|
||||
cmds = (
|
||||
f"create {cname} {cpwd}",
|
||||
f"connect {cname} {cpwd}"
|
||||
|
|
@ -183,7 +183,7 @@ def c_digs(client):
|
|||
exitname1 = EXIT_TEMPLATE % client.counter()
|
||||
exitname2 = EXIT_TEMPLATE % client.counter()
|
||||
client.exits.extend([exitname1, exitname2])
|
||||
return ("@dig/tel %s = %s, %s" % (roomname, exitname1, exitname2),)
|
||||
return ("dig/tel %s = %s, %s" % (roomname, exitname1, exitname2),)
|
||||
|
||||
|
||||
def c_creates_obj(client):
|
||||
|
|
@ -191,10 +191,10 @@ def c_creates_obj(client):
|
|||
objname = OBJ_TEMPLATE % client.counter()
|
||||
client.objs.append(objname)
|
||||
cmds = (
|
||||
"@create %s" % objname,
|
||||
'@desc %s = "this is a test object' % objname,
|
||||
"@set %s/testattr = this is a test attribute value." % objname,
|
||||
"@set %s/testattr2 = this is a second test attribute." % objname,
|
||||
"create %s" % objname,
|
||||
'desc %s = "this is a test object' % objname,
|
||||
"set %s/testattr = this is a test attribute value." % objname,
|
||||
"set %s/testattr2 = this is a second test attribute." % objname,
|
||||
)
|
||||
return cmds
|
||||
|
||||
|
|
@ -203,7 +203,7 @@ def c_creates_button(client):
|
|||
"creates example button, storing name on client"
|
||||
objname = TOBJ_TEMPLATE % client.counter()
|
||||
client.objs.append(objname)
|
||||
cmds = ("@create %s:%s" % (objname, TOBJ_TYPECLASS), "@desc %s = test red button!" % objname)
|
||||
cmds = ("create %s:%s" % (objname, TOBJ_TYPECLASS), "desc %s = test red button!" % objname)
|
||||
return cmds
|
||||
|
||||
|
||||
|
|
@ -295,7 +295,7 @@ elif PROFILE == 'normal_builder':
|
|||
(0.1, c_help),
|
||||
(0.01, c_digs),
|
||||
(0.01, c_creates_obj),
|
||||
(0.2, c_moves)
|
||||
(0.2, c_moves),
|
||||
(0.1, c_measure_lag)
|
||||
)
|
||||
elif PROFILE == 'heavy_builder':
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from django.test import TestCase
|
||||
from mock import Mock, patch, mock_open
|
||||
from anything import Something
|
||||
from .dummyrunner_settings import (
|
||||
c_creates_button,
|
||||
c_creates_obj,
|
||||
|
|
@ -29,8 +30,8 @@ class TestDummyrunnerSettings(TestCase):
|
|||
self.client.cid = 1
|
||||
self.client.counter = Mock(return_value=1)
|
||||
self.client.gid = "20171025161153-1"
|
||||
self.client.name = "Dummy-%s" % self.client.gid
|
||||
self.client.password = "password-%s" % self.client.gid
|
||||
self.client.name = "Dummy_%s" % self.client.gid
|
||||
self.client.password = Something,
|
||||
self.client.start_room = "testing_room_start_%s" % self.client.gid
|
||||
self.client.objs = []
|
||||
self.client.exits = []
|
||||
|
|
@ -43,28 +44,25 @@ class TestDummyrunnerSettings(TestCase):
|
|||
self.assertEqual(
|
||||
c_login(self.client),
|
||||
(
|
||||
"create %s %s" % (self.client.name, self.client.password),
|
||||
"connect %s %s" % (self.client.name, self.client.password),
|
||||
"@dig %s" % self.client.start_room,
|
||||
"@teleport %s" % self.client.start_room,
|
||||
"@dig testing_room_1 = exit_1, exit_1",
|
||||
Something, # create
|
||||
Something, # connect
|
||||
"dig %s" % self.client.start_room,
|
||||
"teleport %s" % self.client.start_room,
|
||||
"py from evennia.server.profiling.dummyrunner import DummyRunnerCmdSet;"
|
||||
"self.cmdset.add(DummyRunnerCmdSet, persistent=False)"
|
||||
),
|
||||
)
|
||||
|
||||
def test_c_login_no_dig(self):
|
||||
self.assertEqual(
|
||||
c_login_nodig(self.client),
|
||||
(
|
||||
"create %s %s" % (self.client.name, self.client.password),
|
||||
"connect %s %s" % (self.client.name, self.client.password),
|
||||
),
|
||||
)
|
||||
cmd1, cmd2 = c_login_nodig(self.client)
|
||||
self.assertTrue(cmd1.startswith("create " + self.client.name + " "))
|
||||
self.assertTrue(cmd2.startswith("connect " + self.client.name + " "))
|
||||
|
||||
def test_c_logout(self):
|
||||
self.assertEqual(c_logout(self.client), "@quit")
|
||||
self.assertEqual(c_logout(self.client), ("quit",))
|
||||
|
||||
def perception_method_tests(self, func, verb, alone_suffix=""):
|
||||
self.assertEqual(func(self.client), "%s%s" % (verb, alone_suffix))
|
||||
self.assertEqual(func(self.client), ("%s%s" % (verb, alone_suffix),))
|
||||
self.client.exits = ["exit1", "exit2"]
|
||||
self.assertEqual(func(self.client), ["%s exit1" % verb, "%s exit2" % verb])
|
||||
self.client.objs = ["foo", "bar"]
|
||||
|
|
@ -83,11 +81,11 @@ class TestDummyrunnerSettings(TestCase):
|
|||
def test_c_help(self):
|
||||
self.assertEqual(
|
||||
c_help(self.client),
|
||||
("help", "help @teleport", "help look", "help @tunnel", "help @dig"),
|
||||
("help", "dummyrunner_echo_response"),
|
||||
)
|
||||
|
||||
def test_c_digs(self):
|
||||
self.assertEqual(c_digs(self.client), ("@dig/tel testing_room_1 = exit_1, exit_1"))
|
||||
self.assertEqual(c_digs(self.client), ("dig/tel testing_room_1 = exit_1, exit_1", ))
|
||||
self.assertEqual(self.client.exits, ["exit_1", "exit_1"])
|
||||
self.clear_client_lists()
|
||||
|
||||
|
|
@ -96,10 +94,10 @@ class TestDummyrunnerSettings(TestCase):
|
|||
self.assertEqual(
|
||||
c_creates_obj(self.client),
|
||||
(
|
||||
"@create %s" % objname,
|
||||
'@desc %s = "this is a test object' % objname,
|
||||
"@set %s/testattr = this is a test attribute value." % objname,
|
||||
"@set %s/testattr2 = this is a second test attribute." % objname,
|
||||
"create %s" % objname,
|
||||
'desc %s = "this is a test object' % objname,
|
||||
"set %s/testattr = this is a test attribute value." % objname,
|
||||
"set %s/testattr2 = this is a second test attribute." % objname,
|
||||
),
|
||||
)
|
||||
self.assertEqual(self.client.objs, [objname])
|
||||
|
|
@ -110,7 +108,7 @@ class TestDummyrunnerSettings(TestCase):
|
|||
typeclass_name = "contrib.tutorial_examples.red_button.RedButton"
|
||||
self.assertEqual(
|
||||
c_creates_button(self.client),
|
||||
("@create %s:%s" % (objname, typeclass_name), "@desc %s = test red button!" % objname),
|
||||
("create %s:%s" % (objname, typeclass_name), "desc %s = test red button!" % objname),
|
||||
)
|
||||
self.assertEqual(self.client.objs, [objname])
|
||||
self.clear_client_lists()
|
||||
|
|
@ -119,25 +117,23 @@ class TestDummyrunnerSettings(TestCase):
|
|||
self.assertEqual(
|
||||
c_socialize(self.client),
|
||||
(
|
||||
"ooc Hello!",
|
||||
"ooc Testing ...",
|
||||
"ooc Testing ... times 2",
|
||||
"pub Hello!",
|
||||
"say Yo!",
|
||||
"emote stands looking around.",
|
||||
),
|
||||
)
|
||||
|
||||
def test_c_moves(self):
|
||||
self.assertEqual(c_moves(self.client), "look")
|
||||
self.assertEqual(c_moves(self.client), ("look",))
|
||||
self.client.exits = ["south", "north"]
|
||||
self.assertEqual(c_moves(self.client), ["south", "north"])
|
||||
self.clear_client_lists()
|
||||
|
||||
def test_c_move_n(self):
|
||||
self.assertEqual(c_moves_n(self.client), "north")
|
||||
self.assertEqual(c_moves_n(self.client), ("north",))
|
||||
|
||||
def test_c_move_s(self):
|
||||
self.assertEqual(c_moves_s(self.client), "south")
|
||||
self.assertEqual(c_moves_s(self.client), ("south",))
|
||||
|
||||
|
||||
class TestMemPlot(TestCase):
|
||||
|
|
|
|||
|
|
@ -223,6 +223,7 @@ class ANSIParser(object):
|
|||
ansi_xterm256_bright_bg_map += settings.COLOR_ANSI_XTERM256_BRIGHT_BG_EXTRA_MAP
|
||||
|
||||
mxp_re = r"\|lc(.*?)\|lt(.*?)\|le"
|
||||
mxp_url_re = r"\|lu(.*?)\|lt(.*?)\|le"
|
||||
|
||||
# prepare regex matching
|
||||
brightbg_sub = re.compile(
|
||||
|
|
@ -237,6 +238,7 @@ class ANSIParser(object):
|
|||
# xterm256_sub = re.compile(r"|".join([tup[0] for tup in xterm256_map]), re.DOTALL)
|
||||
ansi_sub = re.compile(r"|".join([re.escape(tup[0]) for tup in ansi_map]), re.DOTALL)
|
||||
mxp_sub = re.compile(mxp_re, re.DOTALL)
|
||||
mxp_url_sub = re.compile(mxp_url_re, re.DOTALL)
|
||||
|
||||
# used by regex replacer to correctly map ansi sequences
|
||||
ansi_map_dict = dict(ansi_map)
|
||||
|
|
@ -424,7 +426,9 @@ class ANSIParser(object):
|
|||
string (str): The processed string.
|
||||
|
||||
"""
|
||||
return self.mxp_sub.sub(r"\2", string)
|
||||
string = self.mxp_sub.sub(r"\2", string)
|
||||
string = self.mxp_url_sub.sub(r"\1", string) # replace with url verbatim
|
||||
return string
|
||||
|
||||
def parse_ansi(self, string, strip_ansi=False, xterm256=False, mxp=False):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -145,13 +145,17 @@ class ANSIStringTestCase(TestCase):
|
|||
"""
|
||||
mxp1 = "|lclook|ltat|le"
|
||||
mxp2 = "Start to |lclook here|ltclick somewhere here|le first"
|
||||
mxp3 = "Check out |luhttps://www.example.com|ltmy website|le!"
|
||||
self.assertEqual(15, len(ANSIString(mxp1)))
|
||||
self.assertEqual(53, len(ANSIString(mxp2)))
|
||||
self.assertEqual(53, len(ANSIString(mxp3)))
|
||||
# These would indicate an issue with the tables.
|
||||
self.assertEqual(len(ANSIString(mxp1)), len(ANSIString(mxp1).split("\n")[0]))
|
||||
self.assertEqual(len(ANSIString(mxp2)), len(ANSIString(mxp2).split("\n")[0]))
|
||||
self.assertEqual(len(ANSIString(mxp3)), len(ANSIString(mxp3).split("\n")[0]))
|
||||
self.assertEqual(mxp1, ANSIString(mxp1))
|
||||
self.assertEqual(mxp2, str(ANSIString(mxp2)))
|
||||
self.assertEqual(mxp3, str(ANSIString(mxp3)))
|
||||
|
||||
def test_add(self):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -72,10 +72,11 @@ class TestListToString(TestCase):
|
|||
def test_list_to_string(self):
|
||||
self.assertEqual("1, 2, 3", utils.list_to_string([1, 2, 3], endsep=""))
|
||||
self.assertEqual('"1", "2", "3"', utils.list_to_string([1, 2, 3], endsep="", addquote=True))
|
||||
self.assertEqual("1, 2 and 3", utils.list_to_string([1, 2, 3]))
|
||||
self.assertEqual("1, 2, and 3", utils.list_to_string([1, 2, 3]))
|
||||
self.assertEqual(
|
||||
'"1", "2" and "3"', utils.list_to_string([1, 2, 3], endsep="and", addquote=True)
|
||||
'"1", "2", and "3"', utils.list_to_string([1, 2, 3], endsep="and", addquote=True)
|
||||
)
|
||||
self.assertEqual("1 and 2", utils.list_to_string([1, 2]))
|
||||
|
||||
|
||||
class TestMLen(TestCase):
|
||||
|
|
|
|||
|
|
@ -103,9 +103,10 @@ class TextToHTMLparser(object):
|
|||
)
|
||||
re_dblspace = re.compile(r" {2,}", re.M)
|
||||
re_url = re.compile(
|
||||
r'((?:ftp|www|https?)\W+(?:(?!\.(?:\s|$)|&\w+;)[^"\',;$*^\\(){}<>\[\]\s])+)(\.(?:\s|$)|&\w+;|)'
|
||||
r'(?<!=")((?:ftp|www|https?)\W+(?:(?!\.(?:\s|$)|&\w+;)[^"\',;$*^\\(){}<>\[\]\s])+)(\.(?:\s|$)|&\w+;|)'
|
||||
)
|
||||
re_mxplink = re.compile(r"\|lc(.*?)\|lt(.*?)\|le", re.DOTALL)
|
||||
re_mxpurl = re.compile(r"\|lu(.*?)\|lt(.*?)\|le", re.DOTALL)
|
||||
|
||||
def _sub_bgfg(self, colormatch):
|
||||
# print("colormatch.groups()", colormatch.groups())
|
||||
|
|
@ -290,6 +291,21 @@ class TextToHTMLparser(object):
|
|||
)
|
||||
return val
|
||||
|
||||
def sub_mxp_urls(self, match):
|
||||
"""
|
||||
Helper method to be passed to re.sub,
|
||||
replaces MXP links with HTML code.
|
||||
Args:
|
||||
match (re.Matchobject): Match for substitution.
|
||||
Returns:
|
||||
text (str): Processed text.
|
||||
"""
|
||||
url, text = [grp.replace('"', "\\"") for grp in match.groups()]
|
||||
val = (
|
||||
r"""<a id="mxplink" href="{url}" target="_blank">{text}</a>""".format(url=url, text=text)
|
||||
)
|
||||
return val
|
||||
|
||||
def sub_text(self, match):
|
||||
"""
|
||||
Helper method to be passed to re.sub,
|
||||
|
|
@ -337,6 +353,7 @@ class TextToHTMLparser(object):
|
|||
# convert all ansi to html
|
||||
result = re.sub(self.re_string, self.sub_text, text)
|
||||
result = re.sub(self.re_mxplink, self.sub_mxp_links, result)
|
||||
result = re.sub(self.re_mxpurl, self.sub_mxp_urls, result)
|
||||
result = self.re_color(result)
|
||||
result = self.re_bold(result)
|
||||
result = self.re_underline(result)
|
||||
|
|
|
|||
|
|
@ -381,15 +381,13 @@ def iter_to_str(initer, endsep="and", addquote=False):
|
|||
>>> list_to_string([1,2,3], endsep='')
|
||||
'1, 2, 3'
|
||||
>>> list_to_string([1,2,3], ensdep='and')
|
||||
'1, 2 and 3'
|
||||
'1, 2, and 3'
|
||||
>>> list_to_string([1,2,3], endsep='and', addquote=True)
|
||||
'"1", "2" and "3"'
|
||||
'"1", "2", and "3"'
|
||||
```
|
||||
|
||||
"""
|
||||
if not endsep:
|
||||
endsep = ","
|
||||
else:
|
||||
if endsep:
|
||||
endsep = " " + endsep
|
||||
if not initer:
|
||||
return ""
|
||||
|
|
@ -397,11 +395,15 @@ def iter_to_str(initer, endsep="and", addquote=False):
|
|||
if addquote:
|
||||
if len(initer) == 1:
|
||||
return '"%s"' % initer[0]
|
||||
return ", ".join('"%s"' % v for v in initer[:-1]) + "%s %s" % (endsep, '"%s"' % initer[-1])
|
||||
elif len(initer) == 2:
|
||||
return '"%s"' % ('"%s "' % endsep).join(str(v) for v in initer)
|
||||
return ", ".join('"%s"' % v for v in initer[:-1]) + ",%s %s" % (endsep, '"%s"' % initer[-1])
|
||||
else:
|
||||
if len(initer) == 1:
|
||||
return str(initer[0])
|
||||
return ", ".join(str(v) for v in initer[:-1]) + "%s %s" % (endsep, initer[-1])
|
||||
elif len(initer) == 2:
|
||||
return ("%s " % endsep).join(str(v) for v in initer)
|
||||
return ", ".join(str(v) for v in initer[:-1]) + ",%s %s" % (endsep, initer[-1])
|
||||
|
||||
|
||||
# legacy aliases
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue