"""
Module containing the test cases for the in-game Python system.
"""
from textwrap import dedent
from django.conf import settings
from mock import Mock
from evennia import ScriptDB
from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.objects.objects import ExitCommand
from evennia.utils import ansi, utils
from evennia.utils.create import create_object, create_script
from evennia.utils.test_resources import BaseEvenniaTest
from .callbackhandler import CallbackHandler
from .commands import CmdCallback
# Force settings
settings.EVENTS_CALENDAR = "standard"
# Constants
OLD_EVENTS = {}
[docs]class TestEventHandler(BaseEvenniaTest):
"""
Test cases of the event handler to add, edit or delete events.
"""
[docs] def setUp(self):
"""Create the event handler."""
super().setUp()
self.handler = create_script(
"evennia.contrib.base_systems.ingame_python.scripts.EventHandler"
)
# Copy old events if necessary
if OLD_EVENTS:
self.handler.ndb.events = dict(OLD_EVENTS)
# Alter typeclasses
self.char1.swap_typeclass(
"evennia.contrib.base_systems.ingame_python.typeclasses.EventCharacter"
)
self.char2.swap_typeclass(
"evennia.contrib.base_systems.ingame_python.typeclasses.EventCharacter"
)
self.room1.swap_typeclass(
"evennia.contrib.base_systems.ingame_python.typeclasses.EventRoom"
)
self.room2.swap_typeclass(
"evennia.contrib.base_systems.ingame_python.typeclasses.EventRoom"
)
self.exit.swap_typeclass("evennia.contrib.base_systems.ingame_python.typeclasses.EventExit")
[docs] def tearDown(self):
"""Stop the event handler."""
OLD_EVENTS.clear()
OLD_EVENTS.update(self.handler.ndb.events)
self.handler.delete()
CallbackHandler.script = None
super().tearDown()
[docs] def test_start(self):
"""Simply make sure the handler runs with proper initial values."""
self.assertEqual(self.handler.db.callbacks, {})
self.assertEqual(self.handler.db.to_valid, [])
self.assertEqual(self.handler.db.locked, [])
self.assertEqual(self.handler.db.tasks, {})
self.assertIsNotNone(self.handler.ndb.events)
[docs] def test_add_validation(self):
"""Add a callback while needing validation."""
author = self.char1
self.handler.add_callback(
self.room1, "dummy", "character.db.strength = 40", author=author, valid=False
)
callback = self.handler.get_callbacks(self.room1).get("dummy")
callback = callback[0]
self.assertIsNotNone(callback)
self.assertEqual(callback["author"], author)
self.assertEqual(callback["valid"], False)
# Since this callback is not valid, it should appear in 'to_valid'
self.assertIn((self.room1, "dummy", 0), self.handler.db.to_valid)
# Run this dummy callback (shouldn't do anything)
self.char1.db.strength = 10
locals = {"character": self.char1}
self.assertTrue(self.handler.call(self.room1, "dummy", locals=locals))
self.assertEqual(self.char1.db.strength, 10)
[docs] def test_edit(self):
"""Test editing a callback."""
author = self.char1
self.handler.add_callback(
self.room1, "dummy", "character.db.strength = 60", author=author, valid=True
)
# Edit it right away
self.handler.edit_callback(
self.room1, "dummy", 0, "character.db.strength = 65", author=self.char2, valid=True
)
# Check that the callback was written
callback = self.handler.get_callbacks(self.room1).get("dummy")
callback = callback[0]
self.assertIsNotNone(callback)
self.assertEqual(callback["author"], author)
self.assertEqual(callback["valid"], True)
self.assertEqual(callback["updated_by"], self.char2)
# Run this dummy callback
self.char1.db.strength = 10
locals = {"character": self.char1}
self.assertTrue(self.handler.call(self.room1, "dummy", locals=locals))
self.assertEqual(self.char1.db.strength, 65)
[docs] def test_edit_validation(self):
"""Edit a callback when validation isn't automatic."""
author = self.char1
self.handler.add_callback(
self.room1, "dummy", "character.db.strength = 70", author=author, valid=True
)
# Edit it right away
self.handler.edit_callback(
self.room1, "dummy", 0, "character.db.strength = 80", author=self.char2, valid=False
)
# Run this dummy callback (shouldn't do anything)
self.char1.db.strength = 10
locals = {"character": self.char1}
self.assertTrue(self.handler.call(self.room1, "dummy", locals=locals))
self.assertEqual(self.char1.db.strength, 10)
[docs] def test_del(self):
"""Try to delete a callback."""
# Add 3 callbacks
self.handler.add_callback(
self.room1, "dummy", "character.db.strength = 5", author=self.char1, valid=True
)
self.handler.add_callback(
self.room1, "dummy", "character.db.strength = 8", author=self.char2, valid=False
)
self.handler.add_callback(
self.room1, "dummy", "character.db.strength = 9", author=self.char1, valid=True
)
# Note that the second callback isn't valid
self.assertIn((self.room1, "dummy", 1), self.handler.db.to_valid)
# Lock the third callback
self.handler.db.locked.append((self.room1, "dummy", 2))
# Delete the first callback
self.handler.del_callback(self.room1, "dummy", 0)
# The callback #1 that was to valid should be #0 now
self.assertIn((self.room1, "dummy", 0), self.handler.db.to_valid)
self.assertNotIn((self.room1, "dummy", 1), self.handler.db.to_valid)
# The lock has been updated too
self.assertIn((self.room1, "dummy", 1), self.handler.db.locked)
self.assertNotIn((self.room1, "dummy", 2), self.handler.db.locked)
# Now delete the first (not valid) callback
self.handler.del_callback(self.room1, "dummy", 0)
self.assertEqual(self.handler.db.to_valid, [])
self.assertIn((self.room1, "dummy", 0), self.handler.db.locked)
self.assertNotIn((self.room1, "dummy", 1), self.handler.db.locked)
# Call the remaining callback
self.char1.db.strength = 10
locals = {"character": self.char1}
self.assertTrue(self.handler.call(self.room1, "dummy", locals=locals))
self.assertEqual(self.char1.db.strength, 9)
[docs] def test_accept(self):
"""Accept an callback."""
# Add 2 callbacks
self.handler.add_callback(
self.room1, "dummy", "character.db.strength = 5", author=self.char1, valid=True
)
self.handler.add_callback(
self.room1, "dummy", "character.db.strength = 8", author=self.char2, valid=False
)
# Note that the second callback isn't valid
self.assertIn((self.room1, "dummy", 1), self.handler.db.to_valid)
# Accept the second callback
self.handler.accept_callback(self.room1, "dummy", 1)
callback = self.handler.get_callbacks(self.room1).get("dummy")
callback = callback[1]
self.assertIsNotNone(callback)
self.assertEqual(callback["valid"], True)
# Call the dummy callback
self.char1.db.strength = 10
locals = {"character": self.char1}
self.assertTrue(self.handler.call(self.room1, "dummy", locals=locals))
self.assertEqual(self.char1.db.strength, 8)
[docs] def test_call(self):
"""Test to call amore complex callback."""
self.char1.key = "one"
self.char2.key = "two"
# Add an callback
code = dedent(
"""
if character.key == "one":
character.db.health = 50
else:
character.db.health = 0
""".strip(
"\n"
)
)
self.handler.add_callback(self.room1, "dummy", code, author=self.char1, valid=True)
# Call the dummy callback
self.assertTrue(self.handler.call(self.room1, "dummy", locals={"character": self.char1}))
self.assertEqual(self.char1.db.health, 50)
self.assertTrue(self.handler.call(self.room1, "dummy", locals={"character": self.char2}))
self.assertEqual(self.char2.db.health, 0)
[docs] def test_handler(self):
"""Test the object handler."""
self.assertIsNotNone(self.char1.callbacks)
# Add an callback
callback = self.room1.callbacks.add("dummy", "pass", author=self.char1, valid=True)
self.assertEqual(callback.obj, self.room1)
self.assertEqual(callback.name, "dummy")
self.assertEqual(callback.code, "pass")
self.assertEqual(callback.author, self.char1)
self.assertEqual(callback.valid, True)
self.assertIn([callback], list(self.room1.callbacks.all().values()))
# Edit this very callback
new = self.room1.callbacks.edit(
"dummy", 0, "character.db.say = True", author=self.char1, valid=True
)
self.assertIn([new], list(self.room1.callbacks.all().values()))
self.assertNotIn([callback], list(self.room1.callbacks.all().values()))
# Try to call this callback
self.assertTrue(self.room1.callbacks.call("dummy", locals={"character": self.char2}))
self.assertTrue(self.char2.db.say)
# Delete the callback
self.room1.callbacks.remove("dummy", 0)
self.assertEqual(self.room1.callbacks.all(), {})
[docs]class TestCmdCallback(BaseEvenniaCommandTest):
"""Test the @callback command."""
[docs] def setUp(self):
"""Create the callback handler."""
super().setUp()
self.handler = create_script(
"evennia.contrib.base_systems.ingame_python.scripts.EventHandler"
)
# Copy old events if necessary
if OLD_EVENTS:
self.handler.ndb.events = dict(OLD_EVENTS)
# Alter typeclasses
self.char1.swap_typeclass(
"evennia.contrib.base_systems.ingame_python.typeclasses.EventCharacter"
)
self.char2.swap_typeclass(
"evennia.contrib.base_systems.ingame_python.typeclasses.EventCharacter"
)
self.room1.swap_typeclass(
"evennia.contrib.base_systems.ingame_python.typeclasses.EventRoom"
)
self.room2.swap_typeclass(
"evennia.contrib.base_systems.ingame_python.typeclasses.EventRoom"
)
self.exit.swap_typeclass("evennia.contrib.base_systems.ingame_python.typeclasses.EventExit")
[docs] def tearDown(self):
"""Stop the callback handler."""
OLD_EVENTS.clear()
OLD_EVENTS.update(self.handler.ndb.events)
self.handler.delete()
for script in ScriptDB.objects.filter(
db_typeclass_path="evennia.contrib.base_systems.ingame_python.scripts.TimeEventScript"
):
script.delete()
CallbackHandler.script = None
super().tearDown()
[docs] def test_list(self):
"""Test listing callbacks with different rights."""
table = self.call(CmdCallback(), "out")
lines = table.splitlines()[3:-1]
self.assertNotEqual(lines, [])
# Check that the second column only contains 0 (0) (no callback yet)
for line in lines:
cols = line.split("|")
self.assertIn(cols[2].strip(), ("0 (0)", ""))
# Add some callback
self.handler.add_callback(self.exit, "traverse", "pass", author=self.char1, valid=True)
# Try to obtain more details on a specific callback on exit
table = self.call(CmdCallback(), "out = traverse")
lines = table.splitlines()[3:-1]
self.assertEqual(len(lines), 1)
line = lines[0]
cols = line.split("|")
self.assertIn(cols[1].strip(), ("1", ""))
self.assertIn(cols[2].strip(), (str(self.char1), ""))
self.assertIn(cols[-1].strip(), ("Yes", "No", ""))
# Run the same command with char2
# char2 shouldn't see the last column (Valid)
table = self.call(CmdCallback(), "out = traverse", caller=self.char2)
lines = table.splitlines()[3:-1]
self.assertEqual(len(lines), 1)
line = lines[0]
cols = line.split("|")
self.assertEqual(cols[1].strip(), "1")
self.assertNotIn(cols[-1].strip(), ("Yes", "No"))
# In any case, display the callback
# The last line should be "pass" (the callback code)
details = self.call(CmdCallback(), "out = traverse 1")
self.assertEqual(details.splitlines()[-1], "pass")
[docs] def test_add(self):
"""Test to add an callback."""
self.call(CmdCallback(), "/add out = traverse")
editor = self.char1.ndb._eveditor
self.assertIsNotNone(editor)
# Edit the callback
editor.update_buffer(
dedent(
"""
if character.key == "one":
character.msg("You can pass.")
else:
character.msg("You can't pass.")
deny()
""".strip(
"\n"
)
)
)
editor.save_buffer()
editor.quit()
callback = self.exit.callbacks.get("traverse")[0]
self.assertEqual(callback.author, self.char1)
self.assertEqual(callback.valid, True)
self.assertTrue(len(callback.code) > 0)
# We're going to try the same thing but with char2
# char2 being a player for our test, the callback won't be validated.
self.call(CmdCallback(), "/add out = traverse", caller=self.char2)
editor = self.char2.ndb._eveditor
self.assertIsNotNone(editor)
# Edit the callback
editor.update_buffer(
dedent(
"""
character.msg("No way.")
""".strip(
"\n"
)
)
)
editor.save_buffer()
editor.quit()
callback = self.exit.callbacks.get("traverse")[1]
self.assertEqual(callback.author, self.char2)
self.assertEqual(callback.valid, False)
self.assertTrue(len(callback.code) > 0)
[docs] def test_del(self):
"""Add and remove an callback."""
self.handler.add_callback(self.exit, "traverse", "pass", author=self.char1, valid=True)
# Try to delete the callback
# char2 shouldn't be allowed to do so (that's not HIS callback)
self.call(CmdCallback(), "/del out = traverse 1", caller=self.char2)
self.assertTrue(len(self.handler.get_callbacks(self.exit).get("traverse", [])) == 1)
# Now, char1 should be allowed to delete it
self.call(CmdCallback(), "/del out = traverse 1")
self.assertTrue(len(self.handler.get_callbacks(self.exit).get("traverse", [])) == 0)
[docs] def test_lock(self):
"""Test the lock of multiple editing."""
self.call(CmdCallback(), "/add here = time 8:00", caller=self.char2)
self.assertIsNotNone(self.char2.ndb._eveditor)
# Now ask char1 to edit
line = self.call(CmdCallback(), "/edit here = time 1")
self.assertIsNone(self.char1.ndb._eveditor)
# Try to delete this callback while char2 is editing it
line = self.call(CmdCallback(), "/del here = time 1")
[docs] def test_accept(self):
"""Accept an callback."""
self.call(CmdCallback(), "/add here = time 8:00", caller=self.char2)
editor = self.char2.ndb._eveditor
self.assertIsNotNone(editor)
# Edit the callback
editor.update_buffer(
dedent(
"""
room.msg_contents("It's 8 PM, everybody up!")
""".strip(
"\n"
)
)
)
editor.save_buffer()
editor.quit()
callback = self.room1.callbacks.get("time")[0]
self.assertEqual(callback.valid, False)
# chars shouldn't be allowed to the callback
self.call(CmdCallback(), "/accept here = time 1", caller=self.char2)
callback = self.room1.callbacks.get("time")[0]
self.assertEqual(callback.valid, False)
# char1 will accept the callback
self.call(CmdCallback(), "/accept here = time 1")
callback = self.room1.callbacks.get("time")[0]
self.assertEqual(callback.valid, True)
[docs]class TestDefaultCallbacks(BaseEvenniaCommandTest):
"""Test the default callbacks."""
[docs] def setUp(self):
"""Create the callback handler."""
super().setUp()
self.handler = create_script(
"evennia.contrib.base_systems.ingame_python.scripts.EventHandler"
)
# Copy old events if necessary
if OLD_EVENTS:
self.handler.ndb.events = dict(OLD_EVENTS)
# Alter typeclasses
self.char1.swap_typeclass(
"evennia.contrib.base_systems.ingame_python.typeclasses.EventCharacter"
)
self.char2.swap_typeclass(
"evennia.contrib.base_systems.ingame_python.typeclasses.EventCharacter"
)
self.room1.swap_typeclass(
"evennia.contrib.base_systems.ingame_python.typeclasses.EventRoom"
)
self.room2.swap_typeclass(
"evennia.contrib.base_systems.ingame_python.typeclasses.EventRoom"
)
self.exit.swap_typeclass("evennia.contrib.base_systems.ingame_python.typeclasses.EventExit")
[docs] def tearDown(self):
"""Stop the callback handler."""
OLD_EVENTS.clear()
OLD_EVENTS.update(self.handler.ndb.events)
self.handler.delete()
CallbackHandler.script = None
super().tearDown()
[docs] def test_exit(self):
"""Test the callbacks of an exit."""
self.char1.key = "char1"
code = dedent(
"""
if character.key == "char1":
character.msg("You can leave.")
else:
character.msg("You cannot leave.")
deny()
""".strip(
"\n"
)
)
# Enforce self.exit.destination since swapping typeclass lose it
self.exit.destination = self.room2
# Try the can_traverse callback
self.handler.add_callback(self.exit, "can_traverse", code, author=self.char1, valid=True)
# Have char1 move through the exit
self.call(ExitCommand(), "", "You can leave.", obj=self.exit)
self.assertIs(self.char1.location, self.room2)
# Have char2 move through this exit
self.call(ExitCommand(), "", "You cannot leave.", obj=self.exit, caller=self.char2)
self.assertIs(self.char2.location, self.room1)
# Try the traverse callback
self.handler.del_callback(self.exit, "can_traverse", 0)
self.handler.add_callback(
self.exit, "traverse", "character.msg('Fine!')", author=self.char1, valid=True
)
# Have char2 move through the exit
self.call(ExitCommand(), "", obj=self.exit, caller=self.char2)
self.assertIs(self.char2.location, self.room2)
self.handler.del_callback(self.exit, "traverse", 0)
# Move char1 and char2 back
self.char1.location = self.room1
self.char2.location = self.room1
# Test msg_arrive and msg_leave
code = 'message = "{character} goes out."'
self.handler.add_callback(self.exit, "msg_leave", code, author=self.char1, valid=True)
# Have char1 move through the exit
old_msg = self.char2.msg
try:
self.char2.msg = Mock()
self.call(ExitCommand(), "", obj=self.exit)
stored_msg = [
args[0] if args and args[0] else kwargs.get("text", utils.to_str(kwargs))
for name, args, kwargs in self.char2.msg.mock_calls
]
# Get the first element of a tuple if msg received a tuple instead of a string
stored_msg = [smsg[0] if isinstance(smsg, tuple) else smsg for smsg in stored_msg]
returned_msg = ansi.parse_ansi("\n".join(stored_msg), strip_ansi=True)
self.assertEqual(returned_msg, "char1 goes out.")
finally:
self.char2.msg = old_msg
# Create a return exit
back = create_object(
"evennia.objects.objects.DefaultExit",
key="in",
location=self.room2,
destination=self.room1,
)
code = 'message = "{character} goes in."'
self.handler.add_callback(self.exit, "msg_arrive", code, author=self.char1, valid=True)
# Have char1 move through the exit
old_msg = self.char2.msg
try:
self.char2.msg = Mock()
self.call(ExitCommand(), "", obj=back)
stored_msg = [
args[0] if args and args[0] else kwargs.get("text", utils.to_str(kwargs))
for name, args, kwargs in self.char2.msg.mock_calls
]
# Get the first element of a tuple if msg received a tuple instead of a string
stored_msg = [smsg[0] if isinstance(smsg, tuple) else smsg for smsg in stored_msg]
returned_msg = ansi.parse_ansi("\n".join(stored_msg), strip_ansi=True)
self.assertEqual(returned_msg, "char1 goes in.")
finally:
self.char2.msg = old_msg