Add unit tests for syscmds, refactor cmdparser

This commit is contained in:
Griatch 2019-02-01 23:23:41 +01:00
parent 55eb026e95
commit 11d39a57b5
4 changed files with 170 additions and 119 deletions

View file

@ -15,40 +15,7 @@ _MULTIMATCH_REGEX = re.compile(settings.SEARCH_MULTIMATCH_REGEX, re.I + re.U)
_CMD_IGNORE_PREFIXES = settings.CMD_IGNORE_PREFIXES _CMD_IGNORE_PREFIXES = settings.CMD_IGNORE_PREFIXES
def cmdparser(raw_string, cmdset, caller, match_index=None): def create_match(cmdname, string, cmdobj, raw_cmdname):
"""
This function is called by the cmdhandler once it has
gathered and merged all valid cmdsets valid for this particular parsing.
Args:
raw_string (str): The unparsed text entered by the caller.
cmdset (CmdSet): The merged, currently valid cmdset
caller (Session, Account or Object): The caller triggering this parsing.
match_index (int, optional): Index to pick a given match in a
list of same-named command matches. If this is given, it suggests
this is not the first time this function was called: normally
the first run resulted in a multimatch, and the index is given
to select between the results for the second run.
Notes:
The cmdparser understand the following command combinations (where
[] marks optional parts.
```
[cmdname[ cmdname2 cmdname3 ...] [the rest]
```
A command may consist of any number of space-separated words of any
length, and contain any character. It may also be empty.
The parser makes use of the cmdset to find command candidates. The
parser return a list of matches. Each match is a tuple with its
first three elements being the parsed cmdname (lower case),
the remaining arguments, and the matched cmdobject from the cmdset.
"""
def create_match(cmdname, string, cmdobj, raw_cmdname):
""" """
Builds a command match by splitting the incoming string and Builds a command match by splitting the incoming string and
evaluating the quality of the match. evaluating the quality of the match.
@ -62,12 +29,13 @@ def cmdparser(raw_string, cmdset, caller, match_index=None):
otherwise it is None. otherwise it is None.
Returns: Returns:
match (tuple): This is on the form (cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname), where match (tuple): This is on the form (cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname),
`cmdname` is the command's name and `args` the rest of the incoming string, where `cmdname` is the command's name and `args` is the rest of the incoming
without said command name. `cmdobj` is the Command instance, the cmdlen is string, without said command name. `cmdobj` is
the same as len(cmdname) and mratio is a measure of how big a part of the the Command instance, the cmdlen is the same as len(cmdname) and mratio
full input string the cmdname takes up - an exact match would be 1.0. Finally, is a measure of how big a part of the full input string the cmdname
the `raw_cmdname` is the cmdname unmodified by eventual prefix-stripping. takes up - an exact match would be 1.0. Finally, the `raw_cmdname` is
the cmdname unmodified by eventual prefix-stripping.
""" """
cmdlen, strlen = len(unicode(cmdname)), len(unicode(string)) cmdlen, strlen = len(unicode(cmdname)), len(unicode(string))
@ -75,7 +43,22 @@ def cmdparser(raw_string, cmdset, caller, match_index=None):
args = string[cmdlen:] args = string[cmdlen:]
return (cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname) return (cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname)
def build_matches(raw_string, include_prefixes=False):
def build_matches(raw_string, cmdset, include_prefixes=False):
"""
Build match tuples by matching raw_string against available commands.
Args:
raw_string (str): Input string that can look in any way; the only assumption is
that the sought command's name/alias must be *first* in the string.
cmdset (CmdSet): The current cmdset to pick Commands from.
include_prefixes (bool): If set, include prefixes like @, ! etc (specified in settings)
in the match, otherwise strip them before matching.
Returns:
matches (list) A list of match tuples created by `cmdparser.create_match`.
"""
l_raw_string = raw_string.lower() l_raw_string = raw_string.lower()
matches = [] matches = []
try: try:
@ -99,8 +82,28 @@ def cmdparser(raw_string, cmdset, caller, match_index=None):
log_trace("cmdhandler error. raw_input:%s" % raw_string) log_trace("cmdhandler error. raw_input:%s" % raw_string)
return matches return matches
def try_num_prefixes(raw_string):
if not matches: def try_num_prefixes(raw_string):
"""
Test if user tried to separate multi-matches with a number separator
(default 1-name, 2-name etc). This is usually called last, if no other
match was found.
Args:
raw_string (str): The user input to parse.
Returns:
mindex, new_raw_string (tuple): If a multimatch-separator was detected,
this is stripped out as an integer to separate between the matches. The
new_raw_string is the result of stripping out that identifier. If no
such form was found, returns (None, None).
Example:
In the default configuration, entering 2-ball (e.g. in a room will more
than one 'ball' object), will lead to a multimatch and this function
will parse `"2-ball"` and return `(2, "ball")`.
"""
# no matches found # no matches found
num_ref_match = _MULTIMATCH_REGEX.match(raw_string) num_ref_match = _MULTIMATCH_REGEX.match(raw_string)
if num_ref_match: if num_ref_match:
@ -109,13 +112,52 @@ def cmdparser(raw_string, cmdset, caller, match_index=None):
# contain the groups "number" and "name". # contain the groups "number" and "name".
mindex, new_raw_string = num_ref_match.group("number"), num_ref_match.group("name") mindex, new_raw_string = num_ref_match.group("number"), num_ref_match.group("name")
return mindex, new_raw_string return mindex, new_raw_string
else:
return None, None return None, None
def cmdparser(raw_string, cmdset, caller, match_index=None):
"""
This function is called by the cmdhandler once it has
gathered and merged all valid cmdsets valid for this particular parsing.
Args:
raw_string (str): The unparsed text entered by the caller.
cmdset (CmdSet): The merged, currently valid cmdset
caller (Session, Account or Object): The caller triggering this parsing.
match_index (int, optional): Index to pick a given match in a
list of same-named command matches. If this is given, it suggests
this is not the first time this function was called: normally
the first run resulted in a multimatch, and the index is given
to select between the results for the second run.
Returns:
matches (list): This is a list of match-tuples as returned by `create_match`.
If no matches were found, this is an empty list.
Notes:
The cmdparser understand the following command combinations (where
[] marks optional parts.
```
[cmdname[ cmdname2 cmdname3 ...] [the rest]
```
A command may consist of any number of space-separated words of any
length, and contain any character. It may also be empty.
The parser makes use of the cmdset to find command candidates. The
parser return a list of matches. Each match is a tuple with its
first three elements being the parsed cmdname (lower case),
the remaining arguments, and the matched cmdobject from the cmdset.
"""
if not raw_string: if not raw_string:
return [] return []
# find mathces, first using the full name # find mathces, first using the full name
matches = build_matches(raw_string, include_prefixes=True) matches = build_matches(raw_string, cmdset, include_prefixes=True)
if not matches: if not matches:
# try to match a number 1-cmdname, 2-cmdname etc # try to match a number 1-cmdname, 2-cmdname etc
mindex, new_raw_string = try_num_prefixes(raw_string) mindex, new_raw_string = try_num_prefixes(raw_string)
@ -124,7 +166,7 @@ def cmdparser(raw_string, cmdset, caller, match_index=None):
if _CMD_IGNORE_PREFIXES: if _CMD_IGNORE_PREFIXES:
# still no match. Try to strip prefixes # still no match. Try to strip prefixes
raw_string = raw_string.lstrip(_CMD_IGNORE_PREFIXES) if len(raw_string) > 1 else raw_string raw_string = raw_string.lstrip(_CMD_IGNORE_PREFIXES) if len(raw_string) > 1 else raw_string
matches = build_matches(raw_string, include_prefixes=False) matches = build_matches(raw_string, cmdset, include_prefixes=False)
# only select command matches we are actually allowed to call. # only select command matches we are actually allowed to call.
matches = [match for match in matches if match[2].access(caller, 'cmd')] matches = [match for match in matches if match[2].access(caller, 'cmd')]

View file

@ -20,6 +20,7 @@ the line is just added to the editor buffer).
from evennia.comms.models import ChannelDB from evennia.comms.models import ChannelDB
from evennia.utils import create from evennia.utils import create
from evennia.utils.utils import at_search_result
# The command keys the engine is calling # The command keys the engine is calling
# (the actual names all start with __) # (the actual names all start with __)
@ -76,57 +77,30 @@ class SystemMultimatch(COMMAND_DEFAULT_CLASS):
The cmdhandler adds a special attribute 'matches' to this The cmdhandler adds a special attribute 'matches' to this
system command. system command.
matches = [(candidate, cmd) , (candidate, cmd), ...], matches = [(cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname) , (cmdname, ...), ...]
Here, `cmdname` is the command's name and `args` the rest of the incoming string,
without said command name. `cmdobj` is the Command instance, the cmdlen is
the same as len(cmdname) and mratio is a measure of how big a part of the
full input string the cmdname takes up - an exact match would be 1.0. Finally,
the `raw_cmdname` is the cmdname unmodified by eventual prefix-stripping.
where candidate is an instance of evennia.commands.cmdparser.CommandCandidate
and cmd is an an instantiated Command object matching the candidate.
""" """
key = CMD_MULTIMATCH key = CMD_MULTIMATCH
locks = "cmd:all()" locks = "cmd:all()"
def format_multimatches(self, caller, matches):
"""
Format multiple command matches to a useful error.
This is copied directly from the default method in
evennia.commands.cmdhandler.
"""
string = "There were multiple matches:"
for num, match in enumerate(matches):
# each match is a tuple (candidate, cmd)
candidate, cmd = match
is_channel = hasattr(cmd, "is_channel") and cmd.is_channel
if is_channel:
is_channel = " (channel)"
else:
is_channel = ""
is_exit = hasattr(cmd, "is_exit") and cmd.is_exit
if is_exit and cmd.destination:
is_exit = " (exit to %s)" % cmd.destination
else:
is_exit = ""
id1 = ""
id2 = ""
if not (is_channel or is_exit) and (hasattr(cmd, 'obj') and cmd.obj != caller):
# the command is defined on some other object
id1 = "%s-" % cmd.obj.name
id2 = " (%s-%s)" % (num + 1, candidate.cmdname)
else:
id1 = "%s-" % (num + 1)
id2 = ""
string += "\n %s%s%s%s%s" % (id1, candidate.cmdname, id2, is_channel, is_exit)
return string
def func(self): def func(self):
""" """
argument to cmd is a comma-separated string of Handle multiple-matches by using the at_search_result default handler.
all the clashing matches.
""" """
string = self.format_multimatches(self.caller, self.matches) # this was set by the cmdparser and is a tuple
self.msg(string) # (cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname). See
# evennia.commands.cmdparse.create_match for more details.
matches = self.matches
# at_search_result will itself msg the multimatch options to the caller.
at_search_result(
[match[2] for match in matches], self.caller, query=matches[0][0])
# Command called when the command given at the command line # Command called when the command given at the command line

View file

@ -21,9 +21,12 @@ from mock import Mock, mock
from evennia.commands.default.cmdset_character import CharacterCmdSet from evennia.commands.default.cmdset_character import CharacterCmdSet
from evennia.utils.test_resources import EvenniaTest from evennia.utils.test_resources import EvenniaTest
from evennia.commands.default import help, general, system, admin, account, building, batchprocess, comms, unloggedin from evennia.commands.default import help, general, system, admin, account, building, batchprocess, comms, unloggedin, syscommands
from evennia.commands.cmdparser import build_matches
from evennia.commands.default.muxcommand import MuxCommand from evennia.commands.default.muxcommand import MuxCommand
from evennia.commands.command import Command, InterruptCommand from evennia.commands.command import Command, InterruptCommand
from evennia.commands import cmdparser
from evennia.commands.cmdset import CmdSet
from evennia.utils import ansi, utils, gametime from evennia.utils import ansi, utils, gametime
from evennia.server.sessionhandler import SESSIONS from evennia.server.sessionhandler import SESSIONS
from evennia import search_object from evennia import search_object
@ -657,3 +660,33 @@ class TestUnconnectedCommand(CommandTest):
datetime.datetime.fromtimestamp(gametime.SERVER_START_TIME).ctime(), datetime.datetime.fromtimestamp(gametime.SERVER_START_TIME).ctime(),
SESSIONS.account_count(), utils.get_evennia_version()) SESSIONS.account_count(), utils.get_evennia_version())
self.call(unloggedin.CmdUnconnectedInfo(), "", expected) self.call(unloggedin.CmdUnconnectedInfo(), "", expected)
# Test syscommands
class TestSystemCommands(CommandTest):
def test_simple_defaults(self):
self.call(syscommands.SystemNoInput(), "")
self.call(syscommands.SystemNoMatch(), "Huh?")
def test_multimatch(self):
# set up fake matches and store on command instance
cmdset = CmdSet()
cmdset.add(general.CmdLook())
cmdset.add(general.CmdLook())
matches = cmdparser.build_matches("look", cmdset)
multimatch = syscommands.SystemMultimatch()
multimatch.matches = matches
self.call(multimatch, "look", "")
@mock.patch("evennia.commands.default.syscommands.ChannelDB")
def test_channelcommand(self, mock_channeldb):
channel = mock.MagicMock()
channel.msg = mock.MagicMock()
mock_channeldb.objects.get_channel = mock.MagicMock(return_value=channel)
self.call(syscommands.SystemSendToChannel(), "public:Hello")
channel.msg.assert_called()

View file

@ -1857,7 +1857,9 @@ def at_search_result(matches, caller, query="", quiet=False, **kwargs):
Returns: Returns:
processed_result (Object or None): This is always a single result processed_result (Object or None): This is always a single result
or `None`. If `None`, any error reporting/handling should or `None`. If `None`, any error reporting/handling should
already have happened. already have happened. The returned object is of the type we are
checking multimatches for (e.g. Objects or Commands)
""" """
error = "" error = ""