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): def _get_menu_prototype(caller):
"""Return currently active menu prototype."""
prototype = None prototype = None
if hasattr(caller.ndb._menutree, "olc_prototype"): if hasattr(caller.ndb._menutree, "olc_prototype"):
prototype = 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): 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") 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: if prototype is not None:
prop = prototype.get(prop, '') 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)) 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 = _get_menu_prototype(caller)
prototype[field] = value prototype[field] = value
caller.ndb._menutree.olc_prototype = prototype caller.ndb._menutree.olc_prototype = prototype
@ -70,15 +83,21 @@ def _set_prototype_value(caller, field, value):
def _set_property(caller, raw_string, **kwargs): 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: Args:
caller (Object, Account): The user of the wizard. caller (Object, Account): The user of the wizard.
raw_string (str): Input from user on given node - the new value to set. raw_string (str): Input from user on given node - the new value to set.
Kwargs: 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`. prop (str): Property name to edit with `raw_string`.
processor (callable): Converts `raw_string` to a form suitable for saving. processor (callable): Converts `raw_string` to a form suitable for saving.
next_node (str): Where to redirect to after this has run. next_node (str): Where to redirect to after this has run.
Returns: Returns:
next_node (str): Next node to go to. next_node (str): Next node to go to.
@ -103,7 +122,7 @@ def _set_property(caller, raw_string, **kwargs):
if not value: if not value:
return next_node 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 # typeclass and prototype_parent can't co-exist
if propname_low == "typeclass": if propname_low == "typeclass":
@ -113,16 +132,26 @@ def _set_property(caller, raw_string, **kwargs):
caller.ndb._menutree.olc_prototype = prototype 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 return next_node
def _wizard_options(curr_node, prev_node, next_node, color="|W"): 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 = [] options = []
if prev_node: if prev_node:
options.append({"key": ("|wb|Wack", "b"), options.append({"key": ("|wb|Wack", "b"),
@ -166,7 +195,7 @@ def node_index(caller):
options = [] options = []
options.append( 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"}) "goto": "node_prototype_key"})
for key in ('Prototype', 'Typeclass', 'Key', 'Aliases', 'Attrs', 'Tags', 'Locks', for key in ('Prototype', 'Typeclass', 'Key', 'Aliases', 'Attrs', 'Tags', 'Locks',
'Permissions', 'Location', 'Home', 'Destination'): 'Permissions', 'Location', 'Home', 'Destination'):
@ -178,13 +207,13 @@ def node_index(caller):
cropper = _path_cropper cropper = _path_cropper
options.append( options.append(
{"desc": "|w{}|n{}".format( {"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())}) "goto": "node_{}".format(key.lower())})
required = False required = False
for key in ('Desc', 'Tags', 'Locks'): for key in ('Desc', 'Tags', 'Locks'):
options.append( options.append(
{"desc": "|WPrototype-{}|n|n{}".format( {"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())}) "goto": "node_prototype_{}".format(key.lower())})
return text, options return text, options
@ -215,6 +244,7 @@ def _check_prototype_key(caller, key):
olc_new = _is_new_prototype(caller) olc_new = _is_new_prototype(caller)
key = key.strip().lower() key = key.strip().lower()
if old_prototype: if old_prototype:
old_prototype = old_prototype[0]
# we are starting a new prototype that matches an existing # we are starting a new prototype that matches an existing
if not caller.locks.check_lockstring( if not caller.locks.check_lockstring(
caller, old_prototype['prototype_locks'], access_type='edit'): caller, old_prototype['prototype_locks'], access_type='edit'):
@ -229,7 +259,7 @@ def _check_prototype_key(caller, key):
caller.msg("Prototype already exists. Reloading.") caller.msg("Prototype already exists. Reloading.")
return "node_index" 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): def node_prototype_key(caller):
@ -250,27 +280,32 @@ def node_prototype_key(caller):
return text, options 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"] return [prototype["prototype_key"]
for prototype in protlib.search_prototype() if "prototype_key" in prototype] 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) prototypes = protlib.search_prototype(key=prototype_name)
if prototypes: if prototypes:
caller.msg(protlib.prototype_to_str(prototypes[0])) ret = protlib.prototype_to_str(prototypes[0])
caller.msg(ret)
return ret
else:
caller.msg("Prototype not registered.") caller.msg("Prototype not registered.")
return None
def _prototype_select(caller, prototype): def _prototype_parent_select(caller, prototype):
ret = _set_property(caller, prototype, prop="prototype", processor=str, next_node="node_key") 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)) caller.msg("Selected prototype |y{}|n. Removed any set typeclass parent.".format(prototype))
return ret return ret
@list_node(_all_prototypes, _prototype_select) @list_node(_all_prototype_parents, _prototype_parent_select)
def node_prototype(caller): def node_prototype_parent(caller):
prototype = _get_menu_prototype(caller) prototype = _get_menu_prototype(caller)
prot_parent_key = prototype.get('prototype') prot_parent_key = prototype.get('prototype')
@ -289,18 +324,20 @@ def node_prototype(caller):
text = "\n\n".join(text) text = "\n\n".join(text)
options = _wizard_options("prototype", "prototype_key", "typeclass", color="|W") options = _wizard_options("prototype", "prototype_key", "typeclass", color="|W")
options.append({"key": "_default", options.append({"key": "_default",
"goto": _prototype_examine}) "goto": _prototype_parent_examine})
return text, options return text, options
def _all_typeclasses(caller): def _all_typeclasses(caller):
"""Get name of available typeclasses."""
return list(name for name in return list(name for name in
sorted(utils.get_all_typeclasses("evennia.objects.models.ObjectDB").keys()) sorted(utils.get_all_typeclasses("evennia.objects.models.ObjectDB").keys())
if name != "evennia.objects.models.ObjectDB") if name != "evennia.objects.models.ObjectDB")
def _typeclass_examine(caller, typeclass_path): def _typeclass_examine(caller, typeclass_path):
"""Show info (docstring) about given typeclass."""
if typeclass_path is None: if typeclass_path is None:
# this means we are exiting the listing # this means we are exiting the listing
return "node_key" return "node_key"
@ -319,10 +356,11 @@ def _typeclass_examine(caller, typeclass_path):
else: else:
txt = "This is typeclass |y{}|n.".format(typeclass) txt = "This is typeclass |y{}|n.".format(typeclass)
caller.msg(txt) caller.msg(txt)
return None return txt
def _typeclass_select(caller, typeclass): 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") 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)) caller.msg("Selected typeclass |y{}|n. Removed any set prototype parent.".format(typeclass))
return ret return ret
@ -350,7 +388,7 @@ def node_key(caller):
prototype = _get_menu_prototype(caller) prototype = _get_menu_prototype(caller)
key = prototype.get("key") 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: if key:
text.append("Current key value is '|y{key}|n'.".format(key=key)) text.append("Current key value is '|y{key}|n'.".format(key=key))
else: else:
@ -370,7 +408,7 @@ def node_aliases(caller):
aliases = prototype.get("aliases") aliases = prototype.get("aliases")
text = ["Set the prototype's |yAliases|n. Separate multiple aliases with commas. " text = ["Set the prototype's |yAliases|n. Separate multiple aliases with commas. "
"ill retain case sensitivity."] "they'll retain case sensitivity."]
if aliases: if aliases:
text.append("Current aliases are '|y{aliases}|n'.".format(aliases=aliases)) text.append("Current aliases are '|y{aliases}|n'.".format(aliases=aliases))
else: else:
@ -714,7 +752,7 @@ def start_olc(caller, session=None, prototype=None):
menudata = {"node_index": node_index, menudata = {"node_index": node_index,
"node_validate_prototype": node_validate_prototype, "node_validate_prototype": node_validate_prototype,
"node_prototype_key": node_prototype_key, "node_prototype_key": node_prototype_key,
"node_prototype": node_prototype, "node_prototype_parent": node_prototype_parent,
"node_typeclass": node_typeclass, "node_typeclass": node_typeclass,
"node_key": node_key, "node_key": node_key,
"node_aliases": node_aliases, "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.create import create_script
from evennia.utils.utils import ( from evennia.utils.utils import (
all_from_module, make_iter, is_iter, dbid_to_obj, callables_from_module, 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.locks.lockhandler import validate_lockstring, check_lockstring
from evennia.utils import logger from evennia.utils import logger
from evennia.utils import inlinefuncs 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 value (any): The value to test for a parseable protfunc. Only strings will be parsed for
protfuncs, all other types are returned as-is. protfuncs, all other types are returned as-is.
available_functions (dict, optional): Mapping of name:protfunction to use for this parsing. 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 testing (bool, optional): Passed to protfunc. If in a testing mode, some protfuncs may
behave differently. behave differently.
stacktrace (bool, optional): If set, print the stack parsing process of the protfunc-parser. 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): 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 available_functions = _PROT_FUNCS if available_functions is None else available_functions
# insert $obj(#dbref) for #dbref # insert $obj(#dbref) for #dbref

View file

@ -308,6 +308,91 @@ class TestPrototypeStorage(EvenniaTest):
self.assertTrue(str(unicode(protlib.list_prototypes(self.char1)))) 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( @mock.patch("evennia.prototypes.menus.protlib.search_prototype", new=mock.MagicMock(
return_value=[{"prototype_key": "TestPrototype", return_value=[{"prototype_key": "TestPrototype",
"typeclass": "TypeClassTest", "key": "TestObj"}])) "typeclass": "TypeClassTest", "key": "TestObj"}]))
@ -320,6 +405,7 @@ class TestOLCMenu(TestEvMenu):
startnode = "node_index" startnode = "node_index"
debug_output = True debug_output = True
expect_all_nodes = True
expected_node_texts = { expected_node_texts = {
"node_index": "|c --- Prototype wizard --- |n" "node_index": "|c --- Prototype wizard --- |n"

View file

@ -162,6 +162,20 @@ def clr(*args, **kwargs):
def null(*args, **kwargs): def null(*args, **kwargs):
return args[0] if args else '' 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 = {} _INLINE_FUNCS = {}
# we specify a default nomatch function to use if no matching func was # 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 global _PARSING_CACHE
usecache = False usecache = False
if not available_funcs: if not available_funcs:
available_funcs = _INLINE_FUNCS available_funcs = _INLINE_FUNCS
@ -357,6 +370,7 @@ def parse_inlinefunc(string, strip=False, available_funcs=None, stacktrace=False
except KeyError: except KeyError:
stack.append(available_funcs["nomatch"]) stack.append(available_funcs["nomatch"])
stack.append(funcname) stack.append(funcname)
stack.append(None)
ncallable += 1 ncallable += 1
elif gdict["escaped"]: elif gdict["escaped"]:
# escaped tokens # escaped tokens