From 194eb8e42f87edc79f4624392769a0560f3f652f Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 24 Jun 2018 16:03:48 +0200 Subject: [PATCH] Unit testing/debugging olc menu --- evennia/prototypes/menus.py | 94 ++++++++++++++++++++++---------- evennia/prototypes/prototypes.py | 6 +- evennia/prototypes/tests.py | 86 +++++++++++++++++++++++++++++ evennia/utils/inlinefuncs.py | 16 +++++- 4 files changed, 171 insertions(+), 31 deletions(-) diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index ead299abc7..ff38c3448e 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -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, diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index 18516681b2..2ab3416afe 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -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 diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index 0f48c3780a..49624905c7 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -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" diff --git a/evennia/utils/inlinefuncs.py b/evennia/utils/inlinefuncs.py index d62493c786..85ceeadc8a 100644 --- a/evennia/utils/inlinefuncs.py +++ b/evennia/utils/inlinefuncs.py @@ -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