Unit testing/debugging olc menu

This commit is contained in:
Griatch 2018-06-24 16:03:48 +02:00
parent 9360dc71f1
commit 194eb8e42f
4 changed files with 171 additions and 31 deletions

View file

@ -29,7 +29,7 @@ _MENU_ATTR_LITERAL_EVAL_ERROR = (
def _get_menu_prototype(caller):
"""Return currently active menu prototype."""
prototype = None
if hasattr(caller.ndb._menutree, "olc_prototype"):
prototype = caller.ndb._menutree.olc_prototype
@ -40,11 +40,23 @@ def _get_menu_prototype(caller):
def _is_new_prototype(caller):
"""Check if prototype is marked as new or was loaded from a saved one."""
return hasattr(caller.ndb._menutree, "olc_new")
def _format_property(prop, required=False, prototype=None, cropper=None):
def _format_option_value(prop, required=False, prototype=None, cropper=None):
"""
Format wizard option values.
Args:
prop (str): Name or value to format.
required (bool, optional): The option is required.
prototype (dict, optional): If given, `prop` will be considered a key in this prototype.
cropper (callable, optional): A function to crop the value to a certain width.
Returns:
value (str): The formatted value.
"""
if prototype is not None:
prop = prototype.get(prop, '')
@ -61,7 +73,8 @@ def _format_property(prop, required=False, prototype=None, cropper=None):
return " ({}|n)".format(cropper(out) if cropper else utils.crop(out, _MENU_CROP_WIDTH))
def _set_prototype_value(caller, field, value):
def _set_prototype_value(caller, field, value, parse=True):
"""Set prototype's field in a safe way."""
prototype = _get_menu_prototype(caller)
prototype[field] = value
caller.ndb._menutree.olc_prototype = prototype
@ -70,15 +83,21 @@ def _set_prototype_value(caller, field, value):
def _set_property(caller, raw_string, **kwargs):
"""
Update a property. To be called by the 'goto' option variable.
Add or update a property. To be called by the 'goto' option variable.
Args:
caller (Object, Account): The user of the wizard.
raw_string (str): Input from user on given node - the new value to set.
Kwargs:
test_parse (bool): If set (default True), parse raw_string for protfuncs and obj-refs and
try to run result through literal_eval. The parser will be run in 'testing' mode and any
parsing errors will shown to the user. Note that this is just for testing, the original
given string will be what is inserted.
prop (str): Property name to edit with `raw_string`.
processor (callable): Converts `raw_string` to a form suitable for saving.
next_node (str): Where to redirect to after this has run.
Returns:
next_node (str): Next node to go to.
@ -103,7 +122,7 @@ def _set_property(caller, raw_string, **kwargs):
if not value:
return next_node
prototype = _set_prototype_value(caller, "prototype_key", value)
prototype = _set_prototype_value(caller, prop, value)
# typeclass and prototype_parent can't co-exist
if propname_low == "typeclass":
@ -113,16 +132,26 @@ def _set_property(caller, raw_string, **kwargs):
caller.ndb._menutree.olc_prototype = prototype
caller.msg("Set {prop} to '{value}'.".format(prop=prop, value=str(value)))
out = [" Set {prop} to {value} ({typ}).".format(prop=prop, value=value, typ=type(value))]
if kwargs.get("test_parse", True):
out.append(" Simulating parsing ...")
err, parsed_value = protlib.protfunc_parser(value, testing=True)
if err:
out.append(" |yPython `literal_eval` warning: {}|n".format(err))
if parsed_value != value:
out.append(" |g(Example-)value when parsed ({}):|n {}".format(
type(parsed_value), parsed_value))
else:
out.append(" |gNo change.")
caller.msg("\n".join(out))
return next_node
def _wizard_options(curr_node, prev_node, next_node, color="|W"):
"""
Creates default navigation options available in the wizard.
"""
"""Creates default navigation options available in the wizard."""
options = []
if prev_node:
options.append({"key": ("|wb|Wack", "b"),
@ -166,7 +195,7 @@ def node_index(caller):
options = []
options.append(
{"desc": "|WPrototype-Key|n|n{}".format(_format_property("Key", True, prototype, None)),
{"desc": "|WPrototype-Key|n|n{}".format(_format_option_value("Key", True, prototype, None)),
"goto": "node_prototype_key"})
for key in ('Prototype', 'Typeclass', 'Key', 'Aliases', 'Attrs', 'Tags', 'Locks',
'Permissions', 'Location', 'Home', 'Destination'):
@ -178,13 +207,13 @@ def node_index(caller):
cropper = _path_cropper
options.append(
{"desc": "|w{}|n{}".format(
key, _format_property(key, required, prototype, cropper=cropper)),
key, _format_option_value(key, required, prototype, cropper=cropper)),
"goto": "node_{}".format(key.lower())})
required = False
for key in ('Desc', 'Tags', 'Locks'):
options.append(
{"desc": "|WPrototype-{}|n|n{}".format(
key, _format_property(key, required, prototype, None)),
key, _format_option_value(key, required, prototype, None)),
"goto": "node_prototype_{}".format(key.lower())})
return text, options
@ -215,6 +244,7 @@ def _check_prototype_key(caller, key):
olc_new = _is_new_prototype(caller)
key = key.strip().lower()
if old_prototype:
old_prototype = old_prototype[0]
# we are starting a new prototype that matches an existing
if not caller.locks.check_lockstring(
caller, old_prototype['prototype_locks'], access_type='edit'):
@ -229,7 +259,7 @@ def _check_prototype_key(caller, key):
caller.msg("Prototype already exists. Reloading.")
return "node_index"
return _set_property(caller, key, prop='prototype_key', next_node="node_prototype")
return _set_property(caller, key, prop='prototype_key', next_node="node_prototype_parent")
def node_prototype_key(caller):
@ -250,27 +280,32 @@ def node_prototype_key(caller):
return text, options
def _all_prototypes(caller):
def _all_prototype_parents(caller):
"""Return prototype_key of all available prototypes for listing in menu"""
return [prototype["prototype_key"]
for prototype in protlib.search_prototype() if "prototype_key" in prototype]
def _prototype_examine(caller, prototype_name):
def _prototype_parent_examine(caller, prototype_name):
"""Convert prototype to a string representation for closer inspection"""
prototypes = protlib.search_prototype(key=prototype_name)
if prototypes:
caller.msg(protlib.prototype_to_str(prototypes[0]))
caller.msg("Prototype not registered.")
return None
ret = protlib.prototype_to_str(prototypes[0])
caller.msg(ret)
return ret
else:
caller.msg("Prototype not registered.")
def _prototype_select(caller, prototype):
ret = _set_property(caller, prototype, prop="prototype", processor=str, next_node="node_key")
def _prototype_parent_select(caller, prototype):
ret = _set_property(caller, prototype['prototype_key'],
prop="prototype_parent", processor=str, next_node="node_key")
caller.msg("Selected prototype |y{}|n. Removed any set typeclass parent.".format(prototype))
return ret
@list_node(_all_prototypes, _prototype_select)
def node_prototype(caller):
@list_node(_all_prototype_parents, _prototype_parent_select)
def node_prototype_parent(caller):
prototype = _get_menu_prototype(caller)
prot_parent_key = prototype.get('prototype')
@ -289,18 +324,20 @@ def node_prototype(caller):
text = "\n\n".join(text)
options = _wizard_options("prototype", "prototype_key", "typeclass", color="|W")
options.append({"key": "_default",
"goto": _prototype_examine})
"goto": _prototype_parent_examine})
return text, options
def _all_typeclasses(caller):
"""Get name of available typeclasses."""
return list(name for name in
sorted(utils.get_all_typeclasses("evennia.objects.models.ObjectDB").keys())
if name != "evennia.objects.models.ObjectDB")
def _typeclass_examine(caller, typeclass_path):
"""Show info (docstring) about given typeclass."""
if typeclass_path is None:
# this means we are exiting the listing
return "node_key"
@ -319,10 +356,11 @@ def _typeclass_examine(caller, typeclass_path):
else:
txt = "This is typeclass |y{}|n.".format(typeclass)
caller.msg(txt)
return None
return txt
def _typeclass_select(caller, typeclass):
"""Select typeclass from list and add it to prototype. Return next node to go to."""
ret = _set_property(caller, typeclass, prop='typeclass', processor=str, next_node="node_key")
caller.msg("Selected typeclass |y{}|n. Removed any set prototype parent.".format(typeclass))
return ret
@ -350,7 +388,7 @@ def node_key(caller):
prototype = _get_menu_prototype(caller)
key = prototype.get("key")
text = ["Set the prototype's |yKey|n. This will retain case sensitivity."]
text = ["Set the prototype's name (|yKey|n.) This will retain case sensitivity."]
if key:
text.append("Current key value is '|y{key}|n'.".format(key=key))
else:
@ -370,7 +408,7 @@ def node_aliases(caller):
aliases = prototype.get("aliases")
text = ["Set the prototype's |yAliases|n. Separate multiple aliases with commas. "
"ill retain case sensitivity."]
"they'll retain case sensitivity."]
if aliases:
text.append("Current aliases are '|y{aliases}|n'.".format(aliases=aliases))
else:
@ -714,7 +752,7 @@ def start_olc(caller, session=None, prototype=None):
menudata = {"node_index": node_index,
"node_validate_prototype": node_validate_prototype,
"node_prototype_key": node_prototype_key,
"node_prototype": node_prototype,
"node_prototype_parent": node_prototype_parent,
"node_typeclass": node_typeclass,
"node_key": node_key,
"node_aliases": node_aliases,

View file

@ -13,7 +13,7 @@ from evennia.objects.models import ObjectDB
from evennia.utils.create import create_script
from evennia.utils.utils import (
all_from_module, make_iter, is_iter, dbid_to_obj, callables_from_module,
get_all_typeclasses)
get_all_typeclasses, to_str)
from evennia.locks.lockhandler import validate_lockstring, check_lockstring
from evennia.utils import logger
from evennia.utils import inlinefuncs
@ -64,6 +64,7 @@ def protfunc_parser(value, available_functions=None, testing=False, stacktrace=F
value (any): The value to test for a parseable protfunc. Only strings will be parsed for
protfuncs, all other types are returned as-is.
available_functions (dict, optional): Mapping of name:protfunction to use for this parsing.
If not set, use default sources.
testing (bool, optional): Passed to protfunc. If in a testing mode, some protfuncs may
behave differently.
stacktrace (bool, optional): If set, print the stack parsing process of the protfunc-parser.
@ -86,7 +87,8 @@ def protfunc_parser(value, available_functions=None, testing=False, stacktrace=F
"""
if not isinstance(value, basestring):
return value
value = to_str(value, force_string=True)
available_functions = _PROT_FUNCS if available_functions is None else available_functions
# insert $obj(#dbref) for #dbref

View file

@ -308,6 +308,91 @@ class TestPrototypeStorage(EvenniaTest):
self.assertTrue(str(unicode(protlib.list_prototypes(self.char1))))
class _MockMenu(object):
pass
class TestMenuModule(EvenniaTest):
def setUp(self):
super(TestMenuModule, self).setUp()
# set up fake store
self.caller = self.char1
menutree = _MockMenu()
self.caller.ndb._menutree = menutree
self.test_prot = {"prototype_key": "test_prot",
"prototype_locks": "edit:all();spawn:all()"}
def test_helpers(self):
caller = self.caller
# general helpers
self.assertEqual(olc_menus._get_menu_prototype(caller), {})
self.assertEqual(olc_menus._is_new_prototype(caller), True)
self.assertEqual(
olc_menus._set_prototype_value(caller, "key", "TestKey"), {"key": "TestKey"})
self.assertEqual(olc_menus._get_menu_prototype(caller), {"key": "TestKey"})
self.assertEqual(olc_menus._format_option_value(
"key", required=True, prototype=olc_menus._get_menu_prototype(caller)), " (TestKey|n)")
self.assertEqual(olc_menus._format_option_value(
[1, 2, 3, "foo"], required=True), ' (1, 2, 3, foo|n)')
self.assertEqual(olc_menus._set_property(
caller, "ChangedKey", prop="key", processor=str, next_node="foo"), "foo")
self.assertEqual(olc_menus._get_menu_prototype(caller), {"key": "ChangedKey"})
self.assertEqual(olc_menus._wizard_options(
"ThisNode", "PrevNode", "NextNode"),
[{'goto': 'node_PrevNode', 'key': ('|wb|Wack', 'b'), 'desc': '|W(PrevNode)|n'},
{'goto': 'node_NextNode', 'key': ('|wf|Worward', 'f'), 'desc': '|W(NextNode)|n'},
{'goto': 'node_index', 'key': ('|wi|Wndex', 'i')},
{'goto': ('node_validate_prototype', {'back': 'ThisNode'}),
'key': ('|wv|Walidate prototype', 'v')}])
def test_node_helpers(self):
caller = self.caller
with mock.patch("evennia.prototypes.menus.protlib.search_prototype",
new=mock.MagicMock(return_value=[self.test_prot])):
self.assertEqual(olc_menus._check_prototype_key(caller, "test_prot"),
"node_prototype_parent")
caller.ndb._menutree.olc_new = True
self.assertEqual(olc_menus._check_prototype_key(caller, "test_prot"),
"node_index")
self.assertEqual(olc_menus._all_prototype_parents(caller), ['test_prot'])
self.assertEqual(olc_menus._prototype_parent_examine(
caller, 'test_prot'),
'|cprototype key:|n test_prot, |ctags:|n None, |clocks:|n edit:all();spawn:all() '
'\n|cdesc:|n None \n|cprototype:|n {\n \n}')
self.assertEqual(olc_menus._prototype_parent_select(caller, self.test_prot), "node_key")
self.assertEqual(olc_menus._get_menu_prototype(caller),
{'prototype_key': 'test_prot',
'prototype_locks': 'edit:all();spawn:all()',
'prototype_parent': "test_prot"})
with mock.patch("evennia.utils.utils.get_all_typeclasses",
new=mock.MagicMock(return_value={"foo": None, "bar": None})):
self.assertEqual(olc_menus._all_typeclasses(caller), ["bar", "foo"])
self.assertTrue(olc_menus._typeclass_examine(
caller, "evennia.objects.objects.DefaultObject").startswith("Typeclass |y"))
self.assertEqual(olc_menus._typeclass_select(
caller, "evennia.objects.objects.DefaultObject"), "node_key")
# prototype_parent should be popped off here
self.assertEqual(olc_menus._get_menu_prototype(caller),
{'prototype_key': 'test_prot',
'prototype_locks': 'edit:all();spawn:all()',
'typeclass': 'evennia.objects.objects.DefaultObject'})
@mock.patch("evennia.prototypes.menus.protlib.search_prototype", new=mock.MagicMock(
return_value=[{"prototype_key": "TestPrototype",
"typeclass": "TypeClassTest", "key": "TestObj"}]))
@ -320,6 +405,7 @@ class TestOLCMenu(TestEvMenu):
startnode = "node_index"
debug_output = True
expect_all_nodes = True
expected_node_texts = {
"node_index": "|c --- Prototype wizard --- |n"

View file

@ -162,6 +162,20 @@ def clr(*args, **kwargs):
def null(*args, **kwargs):
return args[0] if args else ''
def nomatch(name, *args, **kwargs):
"""
Default implementation of nomatch returns the function as-is as a string.
"""
kwargs.pop("inlinefunc_stack_depth", None)
kwargs.pop("session")
return "${name}({args}{kwargs})".format(
name=name,
args=",".join(args),
kwargs=",".join("{}={}".format(key, val) for key, val in kwargs.items()))
_INLINE_FUNCS = {}
# we specify a default nomatch function to use if no matching func was
@ -284,7 +298,6 @@ def parse_inlinefunc(string, strip=False, available_funcs=None, stacktrace=False
"""
global _PARSING_CACHE
usecache = False
if not available_funcs:
available_funcs = _INLINE_FUNCS
@ -357,6 +370,7 @@ def parse_inlinefunc(string, strip=False, available_funcs=None, stacktrace=False
except KeyError:
stack.append(available_funcs["nomatch"])
stack.append(funcname)
stack.append(None)
ncallable += 1
elif gdict["escaped"]:
# escaped tokens