evennia/evennia/utils/tests/test_evmenu.py
2022-01-07 16:12:12 +01:00

342 lines
12 KiB
Python

"""
Unit tests for the EvMenu system
This sets up a testing parent for testing EvMenu trees. It is configured by subclassing the
`TestEvMenu` class from this module and setting the class variables to point to the menu that should
be tested and how it should be called.
Without adding any further test methods, the tester will process all nodes of the menu, depth first,
by stepping through all options for every node. Optionally, it can check that all nodes are visited.
It will create a hierarchical list of node names that describes the tree structure. This can then be
compared against a template to make sure the menu structure is sound. Easiest way to use this is to
run the test once to see how the structure looks.
The system also allows for testing the returns of each node as part of the parsing.
To help debug the menu, turn on `debug_output`, which will print the traversal process in detail.
"""
import copy
from anything import Anything
from django.test import TestCase
from evennia.utils.test_resources import BaseEvenniaTest
from evennia.utils import evmenu
from evennia.utils import ansi
from mock import MagicMock
class TestEvMenu(TestCase):
"Run the EvMenu testing."
menutree = {} # can also be the path to the menu tree
startnode = "start"
cmdset_mergetype = "Replace"
cmdset_priority = 1
auto_quit = True
auto_look = True
auto_help = True
cmd_on_exit = "look"
persistent = False
startnode_input = ""
kwargs = {}
# if all nodes must be visited for the test to pass. This is not on
# by default since there may be exec-nodes that are made to not be
# visited.
expect_all_nodes = False
# this is compared against the full tree structure generated
expected_tree = []
# this allows for verifying that a given node returns a given text. The
# text is compared with .startswith, so the entire text need not be matched.
expected_node_texts = {}
# just check the number of options from each node
expected_node_options_count = {}
# check the actual options
expected_node_options = {}
# set this to print the traversal as it happens (debugging)
debug_output = False
def _debug_output(self, indent, msg):
if self.debug_output:
print(" " * indent + ansi.strip_ansi(msg))
def _test_menutree(self, menu):
"""
This is a automatic tester of the menu tree by recursively progressing through the
structure.
"""
def _depth_first(menu, tree, visited, indent):
# we are in a given node here
nodename = menu.nodename
options = menu.test_options
if isinstance(options, dict):
options = (options,)
# run validation tests for this node
compare_text = self.expected_node_texts.get(nodename, None)
if compare_text is not None:
compare_text = ansi.strip_ansi(compare_text.strip())
node_text = menu.test_nodetext
self.assertIsNotNone(
bool(node_text),
"node: {}: node-text is None, which was not expected.".format(nodename),
)
if isinstance(node_text, tuple):
node_text, helptext = node_text
node_text = ansi.strip_ansi(node_text.strip())
self.assertTrue(
node_text.startswith(compare_text),
"\nnode \"{}':\nOutput:\n{}\n\nExpected (startswith):\n{}".format(
nodename, node_text, compare_text
),
)
compare_options_count = self.expected_node_options_count.get(nodename, None)
if compare_options_count is not None:
self.assertEqual(
len(options),
compare_options_count,
"Not the right number of options returned from node {}.".format(nodename),
)
compare_options = self.expected_node_options.get(nodename, None)
if compare_options:
self.assertEqual(
options,
compare_options,
"Options returned from node {} does not match.".format(nodename),
)
self._debug_output(indent, "*{}".format(nodename))
subtree = []
if not options:
# an end node
if nodename not in visited:
visited.append(nodename)
subtree = nodename
else:
for inum, optdict in enumerate(options):
key, desc, execute, goto = (
optdict.get("key", ""),
optdict.get("desc", None),
optdict.get("exec", None),
optdict.get("goto", None),
)
# prepare the key to pass to the menu
if isinstance(key, (tuple, list)) and len(key) > 1:
key = key[0]
if key == "_default":
key = "test raw input"
if not key:
key = str(inum + 1)
backup_menu = copy.copy(menu)
# step the menu
menu.parse_input(key)
# from here on we are likely in a different node
nodename = menu.nodename
if menu.close_menu.called:
# this was an end node
self._debug_output(indent, " .. menu exited! Back to previous node.")
menu = backup_menu
menu.close_menu = MagicMock()
visited.append(nodename)
subtree.append(nodename)
elif nodename not in visited:
visited.append(nodename)
subtree.append(nodename)
_depth_first(menu, subtree, visited, indent + 2)
# self._debug_output(indent, " -> arrived at {}".format(nodename))
else:
subtree.append(nodename)
# self._debug_output( indent, " -> arrived at {} (circular call)".format(nodename))
self._debug_output(indent, "-- {} ({}) -> {}".format(key, desc, goto))
if subtree:
tree.append(subtree)
# the start node has already fired at this point
visited_nodes = [menu.nodename]
traversal_tree = [menu.nodename]
_depth_first(menu, traversal_tree, visited_nodes, 1)
if self.expect_all_nodes:
self.assertGreaterEqual(len(menu._menutree), len(visited_nodes))
self.assertEqual(traversal_tree, self.expected_tree)
def setUp(self):
self.menu = None
if self.menutree:
self.caller = MagicMock()
self.caller.key = "Test"
self.caller2 = MagicMock()
self.caller2.key = "Test"
self.caller.msg = MagicMock()
self.caller2.msg = MagicMock()
self.session = MagicMock()
self.session.protocol_flags = {}
self.session2 = MagicMock()
self.session2.protocol_flags = {}
self.caller.session = self.session
self.caller2.session = self.session2
self.menu = evmenu.EvMenu(
self.caller,
self.menutree,
startnode=self.startnode,
cmdset_mergetype=self.cmdset_mergetype,
cmdset_priority=self.cmdset_priority,
auto_quit=self.auto_quit,
auto_look=self.auto_look,
auto_help=self.auto_help,
cmd_on_exit=self.cmd_on_exit,
persistent=False,
startnode_input=self.startnode_input,
session=self.session,
**self.kwargs,
)
# persistent version
self.pmenu = evmenu.EvMenu(
self.caller2,
self.menutree,
startnode=self.startnode,
cmdset_mergetype=self.cmdset_mergetype,
cmdset_priority=self.cmdset_priority,
auto_quit=self.auto_quit,
auto_look=self.auto_look,
auto_help=self.auto_help,
cmd_on_exit=self.cmd_on_exit,
persistent=True,
startnode_input=self.startnode_input,
session=self.session2,
**self.kwargs,
)
self.menu.close_menu = MagicMock()
self.pmenu.close_menu = MagicMock()
def test_menu_structure(self):
if self.menu:
self._test_menutree(self.menu)
self._test_menutree(self.pmenu)
class TestEvMenuExample(TestEvMenu):
menutree = "evennia.utils.tests.data.evmenu_example"
startnode = "test_start_node"
kwargs = {"testval": "val", "testval2": "val2"}
debug_output = False
expected_node_texts = {"test_view_node": "Your name is"}
expected_tree = [
"test_start_node",
[
"test_set_node",
["test_start_node"],
"test_look_node",
["test_start_node"],
"test_view_node",
["test_start_node"],
"test_dynamic_node",
[
"test_dynamic_node",
"test_dynamic_node",
"test_dynamic_node",
"test_dynamic_node",
"test_start_node",
],
"test_end_node",
"test_displayinput_node",
["test_start_node"],
],
]
def test_kwargsave(self):
self.assertTrue(hasattr(self.menu, "testval"))
self.assertTrue(hasattr(self.menu, "testval2"))
def _callnode1(caller, raw_string, **kwargs):
return "node1"
def _callnode2(caller, raw_string, **kwargs):
return "node2"
class TestMenuTemplateParse(BaseEvenniaTest):
"""Test menu templating helpers"""
def setUp(self):
super().setUp()
self.menu_template = """
## node start
Neque ea alias perferendis molestiae eligendi. Debitis exercitationem
exercitationem quas blanditiis quisquam officia ut. Fugit aut fugit enim quia
non. Earum et excepturi animi ex esse accusantium et. Id adipisci eos enim
ratione.
## options
1: first option -> node1
2: second option -> node2
next: node1
## node node1
Node 1
## options
fwd: node2
call1: callnode1()
call2: callnode2(foo=bar, bar=22, goo="another test")
>: start
## node node2
Text of node 2
## options
> foo*: node1
> [0-9]+?: node2
> back: start
"""
self.goto_callables = {"callnode1": _callnode1, "callnode2": _callnode2}
def test_parse_menu_template(self):
"""EvMenu template testing"""
menutree = evmenu.parse_menu_template(self.char1, self.menu_template,
self.goto_callables)
self.assertEqual(menutree, {"start": Anything, "node1": Anything, "node2": Anything})
def test_template2menu(self):
evmenu.template2menu(self.char1, self.menu_template, self.goto_callables)
def test_parse_menu_fail(self):
template = """
## NODE
Text
## OPTIONS
next: callnode2(invalid)
"""
with self.assertRaises(RuntimeError):
evmenu.parse_menu_template(self.char1, template, self.goto_callables)