914 lines
43 KiB
Python
914 lines
43 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
** OBS - this is not a normal command module! **
|
|
** You cannot import anything in this module as a command! **
|
|
|
|
This is part of the Evennia unittest framework, for testing the
|
|
stability and integrity of the codebase during updates. This module
|
|
test the default command set. It is instantiated by the
|
|
evennia/objects/tests.py module, which in turn is run by as part of the
|
|
main test suite started with
|
|
> python game/manage.py test.
|
|
|
|
"""
|
|
import re
|
|
import types
|
|
import datetime
|
|
from anything import Anything
|
|
|
|
from django.conf import settings
|
|
from mock import Mock, mock
|
|
|
|
from evennia import DefaultRoom, DefaultExit
|
|
from evennia.commands.default.cmdset_character import CharacterCmdSet
|
|
from evennia.utils.test_resources import EvenniaTest
|
|
from evennia.commands.default import help, general, system, admin, account, building, batchprocess, comms, unloggedin, syscommands
|
|
from evennia.commands.cmdparser import build_matches
|
|
from evennia.commands.default.muxcommand import MuxCommand
|
|
from evennia.commands.command import Command, InterruptCommand
|
|
from evennia.commands import cmdparser
|
|
from evennia.commands.cmdset import CmdSet
|
|
from evennia.utils import ansi, utils, gametime
|
|
from evennia.server.sessionhandler import SESSIONS
|
|
from evennia import search_object
|
|
from evennia import DefaultObject, DefaultCharacter
|
|
from evennia.prototypes import prototypes as protlib
|
|
|
|
|
|
# set up signal here since we are not starting the server
|
|
|
|
_RE = re.compile(r"^\+|-+\+|\+-+|--+|\|(?:\s|$)", re.MULTILINE)
|
|
|
|
|
|
# ------------------------------------------------------------
|
|
# Command testing
|
|
# ------------------------------------------------------------
|
|
|
|
|
|
class CommandTest(EvenniaTest):
|
|
"""
|
|
Tests a command
|
|
"""
|
|
def call(self, cmdobj, args, msg=None, cmdset=None, noansi=True, caller=None,
|
|
receiver=None, cmdstring=None, obj=None, inputs=None, raw_string=None):
|
|
"""
|
|
Test a command by assigning all the needed
|
|
properties to cmdobj and running
|
|
cmdobj.at_pre_cmd()
|
|
cmdobj.parse()
|
|
cmdobj.func()
|
|
cmdobj.at_post_cmd()
|
|
The msgreturn value is compared to eventual
|
|
output sent to caller.msg in the game
|
|
|
|
Returns:
|
|
msg (str): The received message that was sent to the caller.
|
|
|
|
"""
|
|
caller = caller if caller else self.char1
|
|
receiver = receiver if receiver else caller
|
|
cmdobj.caller = caller
|
|
cmdobj.cmdname = cmdstring if cmdstring else cmdobj.key
|
|
cmdobj.raw_cmdname = cmdobj.cmdname
|
|
cmdobj.cmdstring = cmdobj.cmdname # deprecated
|
|
cmdobj.args = args
|
|
cmdobj.cmdset = cmdset
|
|
cmdobj.session = SESSIONS.session_from_sessid(1)
|
|
cmdobj.account = self.account
|
|
cmdobj.raw_string = raw_string if raw_string is not None else cmdobj.key + " " + args
|
|
cmdobj.obj = obj or (caller if caller else self.char1)
|
|
# test
|
|
old_msg = receiver.msg
|
|
inputs = inputs or []
|
|
|
|
try:
|
|
receiver.msg = Mock()
|
|
if cmdobj.at_pre_cmd():
|
|
return
|
|
cmdobj.parse()
|
|
ret = cmdobj.func()
|
|
|
|
# handle func's with yield in them (generators)
|
|
if isinstance(ret, types.GeneratorType):
|
|
while True:
|
|
try:
|
|
inp = inputs.pop() if inputs else None
|
|
if inp:
|
|
try:
|
|
ret.send(inp)
|
|
except TypeError:
|
|
next(ret)
|
|
ret = ret.send(inp)
|
|
else:
|
|
next(ret)
|
|
except StopIteration:
|
|
break
|
|
|
|
cmdobj.at_post_cmd()
|
|
except StopIteration:
|
|
pass
|
|
except InterruptCommand:
|
|
pass
|
|
|
|
# clean out evtable sugar. We only operate on text-type
|
|
stored_msg = [args[0] if args and args[0] else kwargs.get("text", utils.to_str(kwargs))
|
|
for name, args, kwargs in receiver.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]
|
|
if msg is not None:
|
|
# set our separator for returned messages based on parsing ansi or not
|
|
msg_sep = "|" if noansi else "||"
|
|
# Have to strip ansi for each returned message for the regex to handle it correctly
|
|
returned_msg = msg_sep.join(_RE.sub("", ansi.parse_ansi(mess, strip_ansi=noansi))
|
|
for mess in stored_msg).strip()
|
|
if msg == "" and returned_msg or not returned_msg.startswith(msg.strip()):
|
|
sep1 = "\n" + "=" * 30 + "Wanted message" + "=" * 34 + "\n"
|
|
sep2 = "\n" + "=" * 30 + "Returned message" + "=" * 32 + "\n"
|
|
sep3 = "\n" + "=" * 78
|
|
retval = sep1 + msg.strip() + sep2 + returned_msg + sep3
|
|
raise AssertionError(retval)
|
|
else:
|
|
returned_msg = "\n".join(str(msg) for msg in stored_msg)
|
|
returned_msg = ansi.parse_ansi(returned_msg, strip_ansi=noansi).strip()
|
|
receiver.msg = old_msg
|
|
|
|
return returned_msg
|
|
|
|
# ------------------------------------------------------------
|
|
# Individual module Tests
|
|
# ------------------------------------------------------------
|
|
|
|
|
|
class TestGeneral(CommandTest):
|
|
def test_look(self):
|
|
rid = self.room1.id
|
|
self.call(general.CmdLook(), "here", "Room(#{})\nroom_desc".format(rid))
|
|
|
|
def test_home(self):
|
|
self.call(general.CmdHome(), "", "You are already home")
|
|
|
|
def test_inventory(self):
|
|
self.call(general.CmdInventory(), "", "You are not carrying anything.")
|
|
|
|
def test_pose(self):
|
|
self.call(general.CmdPose(), "looks around", "Char looks around")
|
|
|
|
def test_nick(self):
|
|
self.call(general.CmdNick(), "testalias = testaliasedstring1",
|
|
"Inputline-nick 'testalias' mapped to 'testaliasedstring1'.")
|
|
self.call(general.CmdNick(), "/account testalias = testaliasedstring2",
|
|
"Account-nick 'testalias' mapped to 'testaliasedstring2'.")
|
|
self.call(general.CmdNick(), "/object testalias = testaliasedstring3",
|
|
"Object-nick 'testalias' mapped to 'testaliasedstring3'.")
|
|
self.assertEqual("testaliasedstring1", self.char1.nicks.get("testalias"))
|
|
self.assertEqual("testaliasedstring2", self.char1.nicks.get("testalias", category="account"))
|
|
self.assertEqual(None, self.char1.account.nicks.get("testalias", category="account"))
|
|
self.assertEqual("testaliasedstring3", self.char1.nicks.get("testalias", category="object"))
|
|
|
|
def test_get_and_drop(self):
|
|
self.call(general.CmdGet(), "Obj", "You pick up Obj.")
|
|
self.call(general.CmdDrop(), "Obj", "You drop Obj.")
|
|
|
|
def test_give(self):
|
|
self.call(general.CmdGive(), "Obj to Char2", "You aren't carrying Obj.")
|
|
self.call(general.CmdGive(), "Obj = Char2", "You aren't carrying Obj.")
|
|
self.call(general.CmdGet(), "Obj", "You pick up Obj.")
|
|
self.call(general.CmdGive(), "Obj to Char2", "You give")
|
|
self.call(general.CmdGive(), "Obj = Char", "You give", caller=self.char2)
|
|
|
|
def test_mux_command(self):
|
|
|
|
class CmdTest(MuxCommand):
|
|
key = 'test'
|
|
switch_options = ('test', 'testswitch', 'testswitch2')
|
|
|
|
def func(self):
|
|
self.msg("Switches matched: {}".format(self.switches))
|
|
|
|
self.call(CmdTest(), "/test/testswitch/testswitch2", "Switches matched: ['test', 'testswitch', 'testswitch2']")
|
|
self.call(CmdTest(), "/test", "Switches matched: ['test']")
|
|
self.call(CmdTest(), "/test/testswitch", "Switches matched: ['test', 'testswitch']")
|
|
self.call(CmdTest(), "/testswitch/testswitch2", "Switches matched: ['testswitch', 'testswitch2']")
|
|
self.call(CmdTest(), "/testswitch", "Switches matched: ['testswitch']")
|
|
self.call(CmdTest(), "/testswitch2", "Switches matched: ['testswitch2']")
|
|
self.call(CmdTest(), "/t", "test: Ambiguous switch supplied: "
|
|
"Did you mean /test or /testswitch or /testswitch2?|Switches matched: []")
|
|
self.call(CmdTest(), "/tests", "test: Ambiguous switch supplied: "
|
|
"Did you mean /testswitch or /testswitch2?|Switches matched: []")
|
|
|
|
def test_say(self):
|
|
self.call(general.CmdSay(), "Testing", "You say, \"Testing\"")
|
|
|
|
def test_whisper(self):
|
|
self.call(general.CmdWhisper(), "Obj = Testing", "You whisper to Obj, \"Testing\"", caller=self.char2)
|
|
|
|
def test_access(self):
|
|
self.call(general.CmdAccess(), "", "Permission Hierarchy (climbing):")
|
|
|
|
|
|
class TestHelp(CommandTest):
|
|
def test_help(self):
|
|
self.call(help.CmdHelp(), "", "Command help entries", cmdset=CharacterCmdSet())
|
|
|
|
def test_set_help(self):
|
|
self.call(help.CmdSetHelp(), "testhelp, General = This is a test", "Topic 'testhelp' was successfully created.")
|
|
self.call(help.CmdHelp(), "testhelp", "Help for testhelp", cmdset=CharacterCmdSet())
|
|
|
|
|
|
class TestSystem(CommandTest):
|
|
def test_py(self):
|
|
# we are not testing CmdReload, CmdReset and CmdShutdown, CmdService or CmdTime
|
|
# since the server is not running during these tests.
|
|
self.call(system.CmdPy(), "1+2", ">>> 1+2|3")
|
|
|
|
def test_scripts(self):
|
|
self.call(system.CmdScripts(), "", "dbref ")
|
|
|
|
def test_objects(self):
|
|
self.call(system.CmdObjects(), "", "Object subtype totals")
|
|
|
|
def test_about(self):
|
|
self.call(system.CmdAbout(), "", None)
|
|
|
|
def test_server_load(self):
|
|
self.call(system.CmdServerLoad(), "", "Server CPU and Memory load:")
|
|
|
|
|
|
class TestAdmin(CommandTest):
|
|
def test_emit(self):
|
|
self.call(admin.CmdEmit(), "Char2 = Test", "Emitted to Char2:\nTest")
|
|
|
|
def test_perm(self):
|
|
self.call(admin.CmdPerm(), "Obj = Builder", "Permission 'Builder' given to Obj (the Object/Character).")
|
|
self.call(admin.CmdPerm(), "Char2 = Builder", "Permission 'Builder' given to Char2 (the Object/Character).")
|
|
|
|
def test_wall(self):
|
|
self.call(admin.CmdWall(), "Test", "Announcing to all connected sessions ...")
|
|
|
|
def test_ban(self):
|
|
self.call(admin.CmdBan(), "Char", "Name-Ban char was added.")
|
|
|
|
def test_force(self):
|
|
cid = self.char2.id
|
|
self.call(admin.CmdForce(), "Char2=say test",
|
|
'Char2(#{}) says, "test"|You have forced Char2 to: say test'.format(cid))
|
|
|
|
|
|
class TestAccount(CommandTest):
|
|
|
|
def test_ooc_look(self):
|
|
if settings.MULTISESSION_MODE < 2:
|
|
self.call(account.CmdOOCLook(), "", "You are out-of-character (OOC).", caller=self.account)
|
|
if settings.MULTISESSION_MODE == 2:
|
|
self.call(account.CmdOOCLook(), "", "Account TestAccount (you are OutofCharacter)", caller=self.account)
|
|
|
|
def test_ooc(self):
|
|
self.call(account.CmdOOC(), "", "You go OOC.", caller=self.account)
|
|
|
|
def test_ic(self):
|
|
self.account.unpuppet_object(self.session)
|
|
self.call(account.CmdIC(), "Char", "You become Char.", caller=self.account, receiver=self.char1)
|
|
|
|
def test_password(self):
|
|
self.call(account.CmdPassword(), "testpassword = testpassword", "Password changed.", caller=self.account)
|
|
|
|
def test_option(self):
|
|
self.call(account.CmdOption(), "", "Client settings", caller=self.account)
|
|
|
|
def test_who(self):
|
|
self.call(account.CmdWho(), "", "Accounts:", caller=self.account)
|
|
|
|
def test_quit(self):
|
|
self.call(account.CmdQuit(), "", "Quitting. Hope to see you again, soon.", caller=self.account)
|
|
|
|
def test_sessions(self):
|
|
self.call(account.CmdSessions(), "", "Your current session(s):", caller=self.account)
|
|
|
|
def test_color_test(self):
|
|
self.call(account.CmdColorTest(), "ansi", "ANSI colors:", caller=self.account)
|
|
|
|
def test_char_create(self):
|
|
self.call(account.CmdCharCreate(), "Test1=Test char",
|
|
"Created new character Test1. Use @ic Test1 to enter the game", caller=self.account)
|
|
|
|
def test_char_delete(self):
|
|
# Chardelete requires user input; this test is mainly to confirm
|
|
# whether permissions are being checked
|
|
|
|
# Add char to account playable characters
|
|
self.account.db._playable_characters.append(self.char1)
|
|
|
|
# Try deleting as Developer
|
|
self.call(account.CmdCharDelete(), "Char", "This will permanently destroy 'Char'. This cannot be undone. Continue yes/[no]?", caller=self.account)
|
|
|
|
# Downgrade permissions on account
|
|
self.account.permissions.add('Player')
|
|
self.account.permissions.remove('Developer')
|
|
|
|
# Set lock on character object to prevent deletion
|
|
self.char1.locks.add('delete:none()')
|
|
|
|
# Try deleting as Player
|
|
self.call(account.CmdCharDelete(), "Char", "You do not have permission to delete this character.", caller=self.account)
|
|
|
|
# Set lock on character object to allow self-delete
|
|
self.char1.locks.add('delete:pid(%i)' % self.account.id)
|
|
|
|
# Try deleting as Player again
|
|
self.call(account.CmdCharDelete(), "Char", "This will permanently destroy 'Char'. This cannot be undone. Continue yes/[no]?", caller=self.account)
|
|
|
|
def test_quell(self):
|
|
self.call(account.CmdQuell(), "", "Quelling to current puppet's permissions (developer).", caller=self.account)
|
|
|
|
|
|
class TestBuilding(CommandTest):
|
|
def test_create(self):
|
|
name = settings.BASE_OBJECT_TYPECLASS.rsplit('.', 1)[1]
|
|
self.call(building.CmdCreate(), "/d TestObj1", # /d switch is abbreviated form of /drop
|
|
"You create a new %s: TestObj1." % name)
|
|
self.call(building.CmdCreate(), "", "Usage: ")
|
|
self.call(building.CmdCreate(), "TestObj1;foo;bar",
|
|
"You create a new %s: TestObj1 (aliases: foo, bar)." % name)
|
|
|
|
def test_examine(self):
|
|
self.call(building.CmdExamine(), "", "Name/key: Room")
|
|
self.call(building.CmdExamine(), "Obj", "Name/key: Obj")
|
|
self.call(building.CmdExamine(), "Obj", "Name/key: Obj")
|
|
self.call(building.CmdExamine(), "*TestAccount", "Name/key: TestAccount")
|
|
|
|
self.char1.db.test = "testval"
|
|
self.call(building.CmdExamine(), "self/test", "Persistent attributes:\n test = testval")
|
|
self.call(building.CmdExamine(), "NotFound", "Could not find 'NotFound'.")
|
|
self.call(building.CmdExamine(), "out", "Name/key: out")
|
|
|
|
self.room1.scripts.add(self.script.__class__)
|
|
self.call(building.CmdExamine(), "")
|
|
self.account.scripts.add(self.script.__class__)
|
|
self.call(building.CmdExamine(), "*TestAccount")
|
|
|
|
def test_set_obj_alias(self):
|
|
oid = self.obj1.id
|
|
self.call(building.CmdSetObjAlias(), "Obj =", "Cleared aliases from Obj")
|
|
self.call(building.CmdSetObjAlias(), "Obj = TestObj1b",
|
|
"Alias(es) for 'Obj(#{})' set to 'testobj1b'.".format(oid))
|
|
self.call(building.CmdSetObjAlias(), "", "Usage: ")
|
|
self.call(building.CmdSetObjAlias(), "NotFound =", "Could not find 'NotFound'.")
|
|
|
|
self.call(building.CmdSetObjAlias(), "Obj",
|
|
"Aliases for Obj(#{}): 'testobj1b'".format(oid))
|
|
self.call(building.CmdSetObjAlias(), "Obj2 =", "Cleared aliases from Obj2")
|
|
self.call(building.CmdSetObjAlias(), "Obj2 =", "No aliases to clear.")
|
|
|
|
def test_copy(self):
|
|
self.call(building.CmdCopy(), "Obj = TestObj2;TestObj2b, TestObj3;TestObj3b",
|
|
"Copied Obj to 'TestObj3' (aliases: ['TestObj3b']")
|
|
self.call(building.CmdCopy(), "", "Usage: ")
|
|
self.call(building.CmdCopy(), "Obj", "Identical copy of Obj, named 'Obj_copy' was created.")
|
|
self.call(building.CmdCopy(), "NotFound = Foo", "Could not find 'NotFound'.")
|
|
|
|
def test_attribute_commands(self):
|
|
self.call(building.CmdSetAttribute(), "", "Usage: ")
|
|
self.call(building.CmdSetAttribute(), "Obj/test1=\"value1\"", "Created attribute Obj/test1 = 'value1'")
|
|
self.call(building.CmdSetAttribute(), "Obj2/test2=\"value2\"", "Created attribute Obj2/test2 = 'value2'")
|
|
self.call(building.CmdSetAttribute(), "Obj2/test2", "Attribute Obj2/test2 = value2")
|
|
self.call(building.CmdSetAttribute(), "Obj2/NotFound", "Obj2 has no attribute 'notfound'.")
|
|
|
|
with mock.patch("evennia.commands.default.building.EvEditor") as mock_ed:
|
|
self.call(building.CmdSetAttribute(), "/edit Obj2/test3")
|
|
mock_ed.assert_called_with(self.char1, Anything, Anything, key='Obj2/test3')
|
|
|
|
self.call(building.CmdSetAttribute(), "Obj2/test3=\"value3\"", "Created attribute Obj2/test3 = 'value3'")
|
|
self.call(building.CmdSetAttribute(), "Obj2/test3 = ", "Deleted attribute 'test3' (= True) from Obj2.")
|
|
|
|
self.call(building.CmdCpAttr(), "/copy Obj2/test2 = Obj2/test3",
|
|
"@cpattr: Extra switch \"/copy\" ignored.|\nCopied Obj2.test2 -> Obj2.test3. "
|
|
"(value: 'value2')")
|
|
self.call(building.CmdMvAttr(), "", "Usage: ")
|
|
self.call(building.CmdMvAttr(), "Obj2/test2 = Obj/test3", "Moved Obj2.test2 -> Obj.test3")
|
|
self.call(building.CmdCpAttr(), "", "Usage: ")
|
|
self.call(building.CmdCpAttr(), "Obj/test1 = Obj2/test3", "Copied Obj.test1 -> Obj2.test3")
|
|
|
|
self.call(building.CmdWipe(), "", "Usage: ")
|
|
self.call(building.CmdWipe(), "Obj2/test2/test3", "Wiped attributes test2,test3 on Obj2.")
|
|
self.call(building.CmdWipe(), "Obj2", "Wiped all attributes on Obj2.")
|
|
|
|
def test_name(self):
|
|
self.call(building.CmdName(), "", "Usage: ")
|
|
self.call(building.CmdName(), "Obj2=Obj3", "Object's name changed to 'Obj3'.")
|
|
self.call(building.CmdName(), "*TestAccount=TestAccountRenamed",
|
|
"Account's name changed to 'TestAccountRenamed'.")
|
|
self.call(building.CmdName(), "*NotFound=TestAccountRenamed",
|
|
"Could not find '*NotFound'")
|
|
self.call(building.CmdName(), "Obj3=Obj4;foo;bar",
|
|
"Object's name changed to 'Obj4' (foo, bar).")
|
|
self.call(building.CmdName(), "Obj4=", "No names or aliases defined!")
|
|
|
|
def test_desc(self):
|
|
oid = self.obj2.id
|
|
self.call(building.CmdDesc(), "Obj2=TestDesc",
|
|
"The description was set on Obj2(#{}).".format(oid))
|
|
self.call(building.CmdDesc(), "", "Usage: ")
|
|
|
|
with mock.patch("evennia.commands.default.building.EvEditor") as mock_ed:
|
|
self.call(building.CmdDesc(), "/edit")
|
|
mock_ed.assert_called_with(self.char1, key='desc',
|
|
loadfunc=building._desc_load,
|
|
quitfunc=building._desc_quit,
|
|
savefunc=building._desc_save,
|
|
persistent=True)
|
|
|
|
def test_empty_desc(self):
|
|
"""
|
|
empty desc sets desc as ''
|
|
"""
|
|
oid = self.obj2.id
|
|
o2d = self.obj2.db.desc
|
|
r1d = self.room1.db.desc
|
|
self.call(building.CmdDesc(), "Obj2=",
|
|
"The description was set on Obj2(#{}).".format(oid))
|
|
assert self.obj2.db.desc == '' and self.obj2.db.desc != o2d
|
|
assert self.room1.db.desc == r1d
|
|
|
|
def test_desc_default_to_room(self):
|
|
"""no rhs changes room's desc"""
|
|
rid = self.room1.id
|
|
o2d = self.obj2.db.desc
|
|
r1d = self.room1.db.desc
|
|
self.call(building.CmdDesc(), "Obj2",
|
|
"The description was set on Room(#{}).".format(rid))
|
|
assert self.obj2.db.desc == o2d
|
|
assert self.room1.db.desc == 'Obj2' and self.room1.db.desc != r1d
|
|
|
|
def test_destroy(self):
|
|
confirm = building.CmdDestroy.confirm
|
|
building.CmdDestroy.confirm = False
|
|
self.call(building.CmdDestroy(), "", "Usage: ")
|
|
self.call(building.CmdDestroy(), "Obj", "Obj was destroyed.")
|
|
self.call(building.CmdDestroy(), "Obj", "Obj2 was destroyed.")
|
|
self.call(building.CmdDestroy(), "Obj", "Could not find 'Obj'.| (Objects to destroy "
|
|
"must either be local or specified with a unique #dbref.)")
|
|
self.call(building.CmdDestroy(), settings.DEFAULT_HOME,
|
|
"You are trying to delete") # DEFAULT_HOME should not be deleted
|
|
self.char2.location = self.room2
|
|
charid = self.char2.id
|
|
room1id = self.room1.id
|
|
room2id = self.room2.id
|
|
self.call(building.CmdDestroy(), self.room2.dbref,
|
|
"Char2(#{}) arrives to Room(#{}) from Room2(#{}).|Room2 was destroyed.".format(
|
|
charid, room1id, room2id))
|
|
building.CmdDestroy.confirm = confirm
|
|
|
|
def test_destroy_sequence(self):
|
|
confirm = building.CmdDestroy.confirm
|
|
building.CmdDestroy.confirm = False
|
|
self.call(building.CmdDestroy(),
|
|
"{}-{}".format(self.obj1.dbref, self.obj2.dbref),
|
|
"Obj was destroyed.\nObj2 was destroyed.")
|
|
|
|
def test_dig(self):
|
|
self.call(building.CmdDig(), "TestRoom1=testroom;tr,back;b", "Created room TestRoom1")
|
|
self.call(building.CmdDig(), "", "Usage: ")
|
|
|
|
def test_tunnel(self):
|
|
self.call(building.CmdTunnel(), "n = TestRoom2;test2", "Created room TestRoom2")
|
|
self.call(building.CmdTunnel(), "", "Usage: ")
|
|
self.call(building.CmdTunnel(), "foo = TestRoom2;test2", "@tunnel can only understand the")
|
|
self.call(building.CmdTunnel(), "/tel e = TestRoom3;test3", "Created room TestRoom3")
|
|
DefaultRoom.objects.get_family(db_key="TestRoom3")
|
|
exits = DefaultExit.objects.filter_family(db_key__in=("east", "west"))
|
|
self.assertEqual(len(exits), 2)
|
|
|
|
def test_tunnel_exit_typeclass(self):
|
|
self.call(building.CmdTunnel(), "n:evennia.objects.objects.DefaultExit = TestRoom3",
|
|
"Created room TestRoom3")
|
|
|
|
def test_exit_commands(self):
|
|
self.call(building.CmdOpen(), "TestExit1=Room2", "Created new Exit 'TestExit1' from Room to Room2")
|
|
self.call(building.CmdLink(), "TestExit1=Room", "Link created TestExit1 -> Room (one way).")
|
|
self.call(building.CmdUnLink(), "", "Usage: ")
|
|
self.call(building.CmdLink(), "NotFound", "Could not find 'NotFound'.")
|
|
self.call(building.CmdLink(), "TestExit", "TestExit1 is an exit to Room.")
|
|
self.call(building.CmdLink(), "Obj", "Obj is not an exit. Its home location is Room.")
|
|
self.call(building.CmdUnLink(), "TestExit1", "Former exit TestExit1 no longer links anywhere.")
|
|
|
|
self.char1.location = self.room2
|
|
self.call(building.CmdOpen(), "TestExit2=Room", "Created new Exit 'TestExit2' from Room2 to Room.")
|
|
self.call(building.CmdOpen(), "TestExit2=Room", "Exit TestExit2 already exists. It already points to the correct place.")
|
|
|
|
|
|
# ensure it matches locally first
|
|
self.call(building.CmdLink(), "TestExit=Room2", "Link created TestExit2 -> Room2 (one way).")
|
|
self.call(building.CmdLink(), "/twoway TestExit={}".format(self.exit.dbref),
|
|
"Link created TestExit2 (in Room2) <-> out (in Room) (two-way).")
|
|
self.call(building.CmdLink(), "/twoway TestExit={}".format(self.room1.dbref),
|
|
"To create a two-way link, TestExit2 and Room must both have a location ")
|
|
self.call(building.CmdLink(), "/twoway {}={}".format(self.exit.dbref, self.exit.dbref),
|
|
"Cannot link an object to itself.")
|
|
self.call(building.CmdLink(), "", "Usage: ")
|
|
# ensure can still match globally when not a local name
|
|
self.call(building.CmdLink(), "TestExit1=Room2", "Note: TestExit1")
|
|
self.call(building.CmdLink(), "TestExit1=", "Former exit TestExit1 no longer links anywhere.")
|
|
|
|
def test_set_home(self):
|
|
self.call(building.CmdSetHome(), "Obj = Room2", "Home location of Obj was changed from Room")
|
|
self.call(building.CmdSetHome(), "", "Usage: ")
|
|
self.call(building.CmdSetHome(), "self", "Char's current home is Room")
|
|
self.call(building.CmdSetHome(), "Obj", "Obj's current home is Room2")
|
|
self.obj1.home = None
|
|
self.call(building.CmdSetHome(), "Obj = Room2", "Home location of Obj was set to Room")
|
|
|
|
def test_list_cmdsets(self):
|
|
self.call(building.CmdListCmdSets(), "", "<DefaultCharacter (Union, prio 0, perm)>:")
|
|
self.call(building.CmdListCmdSets(), "NotFound", "Could not find 'NotFound'")
|
|
|
|
def test_typeclass(self):
|
|
self.call(building.CmdTypeclass(), "", "Usage: ")
|
|
self.call(building.CmdTypeclass(), "Obj = evennia.objects.objects.DefaultExit",
|
|
"Obj changed typeclass from evennia.objects.objects.DefaultObject "
|
|
"to evennia.objects.objects.DefaultExit.")
|
|
self.call(building.CmdTypeclass(), "Obj2 = evennia.objects.objects.DefaultExit",
|
|
"Obj2 changed typeclass from evennia.objects.objects.DefaultObject "
|
|
"to evennia.objects.objects.DefaultExit.", cmdstring="@swap")
|
|
self.call(building.CmdTypeclass(), "/list Obj", "Core typeclasses")
|
|
self.call(building.CmdTypeclass(), "/show Obj", "Obj's current typeclass is 'evennia.objects.objects.DefaultExit'")
|
|
self.call(building.CmdTypeclass(), "Obj = evennia.objects.objects.DefaultExit",
|
|
"Obj already has the typeclass 'evennia.objects.objects.DefaultExit'. Use /force to override.")
|
|
self.call(building.CmdTypeclass(), "/force Obj = evennia.objects.objects.DefaultExit",
|
|
"Obj updated its existing typeclass ")
|
|
self.call(building.CmdTypeclass(), "Obj = evennia.objects.objects.DefaultObject")
|
|
self.call(building.CmdTypeclass(), "/show Obj", "Obj's current typeclass is 'evennia.objects.objects.DefaultObject'")
|
|
self.call(building.CmdTypeclass(), "Obj",
|
|
"Obj updated its existing typeclass (evennia.objects.objects.DefaultObject).\n"
|
|
"Only the at_object_creation hook was run (update mode). Attributes set before swap were not removed.",
|
|
cmdstring="@update")
|
|
self.call(building.CmdTypeclass(), "/reset/force Obj=evennia.objects.objects.DefaultObject",
|
|
"Obj updated its existing typeclass (evennia.objects.objects.DefaultObject).\n"
|
|
"All object creation hooks were run. All old attributes where deleted before the swap.")
|
|
|
|
def test_lock(self):
|
|
self.call(building.CmdLock(), "", "Usage: ")
|
|
self.call(building.CmdLock(), "Obj = test:all()", "Added lock 'test:all()' to Obj.")
|
|
self.call(building.CmdLock(), "*TestAccount = test:all()", "Added lock 'test:all()' to TestAccount")
|
|
self.call(building.CmdLock(), "Obj/notfound", "Obj has no lock of access type 'notfound'.")
|
|
self.call(building.CmdLock(), "Obj/test", "test:all()")
|
|
self.call(building.CmdLock(), "/view Obj = edit:false()",
|
|
"Switch(es) view can not be used with a lock assignment. "
|
|
"Use e.g. @lock/del objname/locktype instead.")
|
|
self.call(building.CmdLock(), "Obj = control:false()")
|
|
self.call(building.CmdLock(), "Obj = edit:false()")
|
|
self.call(building.CmdLock(), "Obj/test", "You are not allowed to do that.")
|
|
self.obj1.locks.add("control:true()")
|
|
self.call(building.CmdLock(), "Obj", "call:true()") # etc
|
|
self.call(building.CmdLock(), "*TestAccount", "boot:perm(Admin)") # etc
|
|
|
|
def test_find(self):
|
|
rid2 = self.room2.id
|
|
rmax = rid2 + 100
|
|
self.call(building.CmdFind(), "", "Usage: ")
|
|
self.call(building.CmdFind(), "oom2", "One Match")
|
|
self.call(building.CmdFind(), "oom2 = 1-{}".format(rmax), "One Match")
|
|
self.call(building.CmdFind(), "oom2 = 1 {}".format(rmax), "One Match") # space works too
|
|
self.call(building.CmdFind(), "Char2", "One Match", cmdstring="locate")
|
|
self.call(building.CmdFind(), "/ex Char2", # /ex is an ambiguous switch
|
|
"locate: Ambiguous switch supplied: Did you mean /exit or /exact?|",
|
|
cmdstring="locate")
|
|
self.call(building.CmdFind(), "Char2", "One Match", cmdstring="@locate")
|
|
self.call(building.CmdFind(), "/l Char2", "One Match", cmdstring="find") # /l switch is abbreviated form of /loc
|
|
self.call(building.CmdFind(), "Char2", "One Match", cmdstring="@find")
|
|
self.call(building.CmdFind(), "/startswith Room2", "One Match")
|
|
|
|
self.call(building.CmdFind(), self.char1.dbref, "Exact dbref match")
|
|
self.call(building.CmdFind(), "*TestAccount", "Match")
|
|
|
|
self.call(building.CmdFind(), "/char Obj")
|
|
self.call(building.CmdFind(), "/room Obj")
|
|
self.call(building.CmdFind(), "/exit Obj")
|
|
self.call(building.CmdFind(), "/exact Obj", "One Match")
|
|
|
|
def test_script(self):
|
|
self.call(building.CmdScript(), "Obj = ", "No scripts defined on Obj")
|
|
self.call(building.CmdScript(), "Obj = scripts.Script", "Script scripts.Script successfully added")
|
|
self.call(building.CmdScript(), "", "Usage: ")
|
|
self.call(building.CmdScript(), "= Obj", "To create a global script you need @scripts/add <typeclass>.")
|
|
self.call(building.CmdScript(), "Obj = ", "dbref obj")
|
|
|
|
self.call(building.CmdScript(), "/start Obj", "0 scripts started on Obj") # because it's already started
|
|
self.call(building.CmdScript(), "/stop Obj", "Stopping script")
|
|
|
|
self.call(building.CmdScript(), "Obj = scripts.Script", "Script scripts.Script successfully added")
|
|
self.call(building.CmdScript(), "/start Obj = scripts.Script", "Script scripts.Script could not be (re)started.")
|
|
self.call(building.CmdScript(), "/stop Obj = scripts.Script", "Script stopped and removed from object.")
|
|
|
|
def test_teleport(self):
|
|
oid = self.obj1.id
|
|
rid = self.room1.id
|
|
rid2 = self.room2.id
|
|
self.call(building.CmdTeleport(), "", "Usage: ")
|
|
self.call(building.CmdTeleport(), "Obj = Room", "Obj is already at Room.")
|
|
self.call(building.CmdTeleport(), "Obj = NotFound", "Could not find 'NotFound'.|Destination not found.")
|
|
self.call(building.CmdTeleport(),
|
|
"Obj = Room2", "Obj(#{}) is leaving Room(#{}), heading for Room2(#{}).|Teleported Obj -> Room2.".format(
|
|
oid, rid, rid2))
|
|
self.call(building.CmdTeleport(), "NotFound = Room", "Could not find 'NotFound'.")
|
|
self.call(building.CmdTeleport(), "Obj = Obj", "You can't teleport an object inside of itself!")
|
|
|
|
self.call(building.CmdTeleport(), "/tonone Obj2", "Teleported Obj2 -> None-location.")
|
|
self.call(building.CmdTeleport(), "/quiet Room2", "Room2(#{})".format(rid2))
|
|
self.call(building.CmdTeleport(), "/t", # /t switch is abbreviated form of /tonone
|
|
"Cannot teleport a puppeted object (Char, puppeted by TestAccount")
|
|
self.call(building.CmdTeleport(), "/l Room2", # /l switch is abbreviated form of /loc
|
|
"Destination has no location.")
|
|
self.call(building.CmdTeleport(), "/q me to Room2", # /q switch is abbreviated form of /quiet
|
|
"Char is already at Room2.")
|
|
|
|
def test_tag(self):
|
|
self.call(building.CmdTag(), "", "Usage: ")
|
|
|
|
self.call(building.CmdTag(), "Obj = testtag")
|
|
self.call(building.CmdTag(), "Obj = testtag2")
|
|
self.call(building.CmdTag(), "Obj = testtag2:category1")
|
|
self.call(building.CmdTag(), "Obj = testtag3")
|
|
|
|
self.call(building.CmdTag(), "Obj", "Tags on Obj: 'testtag', 'testtag2', "
|
|
"'testtag2' (category: category1), 'testtag3'")
|
|
|
|
self.call(building.CmdTag(), "/search NotFound", "No objects found with tag 'NotFound'.")
|
|
self.call(building.CmdTag(), "/search testtag", "Found 1 object with tag 'testtag':")
|
|
self.call(building.CmdTag(), "/search testtag2", "Found 1 object with tag 'testtag2':")
|
|
self.call(building.CmdTag(), "/search testtag2:category1",
|
|
"Found 1 object with tag 'testtag2' (category: 'category1'):")
|
|
|
|
|
|
self.call(building.CmdTag(), "/del Obj = testtag3", "Removed tag 'testtag3' from Obj.")
|
|
self.call(building.CmdTag(), "/del Obj",
|
|
"Cleared all tags from Obj: testtag, testtag2, testtag2 (category: category1)")
|
|
|
|
def test_spawn(self):
|
|
def getObject(commandTest, objKeyStr):
|
|
# A helper function to get a spawned object and
|
|
# check that it exists in the process.
|
|
query = search_object(objKeyStr)
|
|
commandTest.assertIsNotNone(query)
|
|
commandTest.assertTrue(bool(query))
|
|
obj = query[0]
|
|
commandTest.assertIsNotNone(obj)
|
|
return obj
|
|
|
|
# Tests "@spawn" without any arguments.
|
|
self.call(building.CmdSpawn(), " ", "Usage: @spawn")
|
|
|
|
# Tests "@spawn <prototype_dictionary>" without specifying location.
|
|
|
|
self.call(building.CmdSpawn(),
|
|
"/save {'prototype_key': 'testprot', 'key':'Test Char', "
|
|
"'typeclass':'evennia.objects.objects.DefaultCharacter'}",
|
|
"Saved prototype: testprot", inputs=['y'])
|
|
|
|
self.call(building.CmdSpawn(), "/search ", "Key ")
|
|
self.call(building.CmdSpawn(), "/search test;test2", "")
|
|
|
|
self.call(building.CmdSpawn(),
|
|
"/save {'key':'Test Char', "
|
|
"'typeclass':'evennia.objects.objects.DefaultCharacter'}",
|
|
"To save a prototype it must have the 'prototype_key' set.")
|
|
|
|
self.call(building.CmdSpawn(), "/list", "Key ")
|
|
|
|
self.call(building.CmdSpawn(), 'testprot', "Spawned Test Char")
|
|
# Tests that the spawned object's location is the same as the caharacter's location, since
|
|
# we did not specify it.
|
|
testchar = getObject(self, "Test Char")
|
|
self.assertEqual(testchar.location, self.char1.location)
|
|
testchar.delete()
|
|
|
|
# Test "@spawn <prototype_dictionary>" with a location other than the character's.
|
|
spawnLoc = self.room2
|
|
if spawnLoc == self.char1.location:
|
|
# Just to make sure we use a different location, in case someone changes
|
|
# char1's default location in the future...
|
|
spawnLoc = self.room1
|
|
|
|
self.call(building.CmdSpawn(),
|
|
"{'prototype_key':'GOBLIN', 'typeclass':'evennia.objects.objects.DefaultCharacter', "
|
|
"'key':'goblin', 'location':'%s'}" % spawnLoc.dbref, "Spawned goblin")
|
|
goblin = getObject(self, "goblin")
|
|
# Tests that the spawned object's type is a DefaultCharacter.
|
|
self.assertIsInstance(goblin, DefaultCharacter)
|
|
self.assertEqual(goblin.location, spawnLoc)
|
|
|
|
goblin.delete()
|
|
|
|
# create prototype
|
|
protlib.create_prototype({'key': 'Ball',
|
|
'typeclass': 'evennia.objects.objects.DefaultCharacter',
|
|
'prototype_key': 'testball'})
|
|
|
|
# Tests "@spawn <prototype_name>"
|
|
self.call(building.CmdSpawn(), "testball", "Spawned Ball")
|
|
|
|
ball = getObject(self, "Ball")
|
|
self.assertEqual(ball.location, self.char1.location)
|
|
self.assertIsInstance(ball, DefaultObject)
|
|
ball.delete()
|
|
|
|
# Tests "@spawn/n ..." without specifying a location.
|
|
# Location should be "None".
|
|
self.call(building.CmdSpawn(), "/n 'BALL'", "Spawned Ball") # /n switch is abbreviated form of /noloc
|
|
ball = getObject(self, "Ball")
|
|
self.assertIsNone(ball.location)
|
|
ball.delete()
|
|
|
|
self.call(building.CmdSpawn(),
|
|
"/noloc {'prototype_parent':'TESTBALL', 'prototype_key': 'testball', 'location':'%s'}"
|
|
% spawnLoc.dbref, "Error: Prototype testball tries to parent itself.")
|
|
|
|
# Tests "@spawn/noloc ...", but DO specify a location.
|
|
# Location should be the specified location.
|
|
self.call(building.CmdSpawn(),
|
|
"/noloc {'prototype_parent':'TESTBALL', 'key': 'Ball', 'prototype_key': 'foo', 'location':'%s'}"
|
|
% spawnLoc.dbref, "Spawned Ball")
|
|
ball = getObject(self, "Ball")
|
|
self.assertEqual(ball.location, spawnLoc)
|
|
ball.delete()
|
|
|
|
# test calling spawn with an invalid prototype.
|
|
self.call(building.CmdSpawn(), "'NO_EXIST'", "No prototype named 'NO_EXIST'")
|
|
|
|
# Test listing commands
|
|
self.call(building.CmdSpawn(), "/list", "Key ")
|
|
|
|
# @spawn/edit (missing prototype)
|
|
# brings up olc menu
|
|
msg = self.call(
|
|
building.CmdSpawn(),
|
|
'/edit')
|
|
assert 'Prototype wizard' in msg
|
|
|
|
# @spawn/edit with valid prototype
|
|
# brings up olc menu loaded with prototype
|
|
msg = self.call(
|
|
building.CmdSpawn(),
|
|
'/edit testball')
|
|
assert 'Prototype wizard' in msg
|
|
assert hasattr(self.char1.ndb._menutree, "olc_prototype")
|
|
assert dict == type(self.char1.ndb._menutree.olc_prototype) \
|
|
and 'prototype_key' in self.char1.ndb._menutree.olc_prototype \
|
|
and 'key' in self.char1.ndb._menutree.olc_prototype \
|
|
and 'testball' == self.char1.ndb._menutree.olc_prototype['prototype_key'] \
|
|
and 'Ball' == self.char1.ndb._menutree.olc_prototype['key']
|
|
assert 'Ball' in msg and 'testball' in msg
|
|
|
|
# @spawn/edit with valid prototype (synomym)
|
|
msg = self.call(
|
|
building.CmdSpawn(),
|
|
'/edit BALL')
|
|
assert 'Prototype wizard' in msg
|
|
assert 'Ball' in msg and 'testball' in msg
|
|
|
|
# @spawn/edit with invalid prototype
|
|
msg = self.call(
|
|
building.CmdSpawn(),
|
|
'/edit NO_EXISTS',
|
|
"No prototype 'NO_EXISTS' was found.")
|
|
|
|
# @spawn/examine (missing prototype)
|
|
# lists all prototypes that exist
|
|
msg = self.call(
|
|
building.CmdSpawn(),
|
|
'/examine')
|
|
assert 'testball' in msg and 'testprot' in msg
|
|
|
|
# @spawn/examine with valid prototype
|
|
# prints the prototype
|
|
msg = self.call(
|
|
building.CmdSpawn(),
|
|
'/examine BALL')
|
|
assert 'Ball' in msg and 'testball' in msg
|
|
|
|
# @spawn/examine with invalid prototype
|
|
# shows error
|
|
self.call(
|
|
building.CmdSpawn(),
|
|
'/examine NO_EXISTS',
|
|
"No prototype 'NO_EXISTS' was found.")
|
|
|
|
|
|
class TestComms(CommandTest):
|
|
|
|
def setUp(self):
|
|
super(CommandTest, self).setUp()
|
|
self.call(comms.CmdChannelCreate(), "testchan;test=Test Channel",
|
|
"Created channel testchan and connected to it.", receiver=self.account)
|
|
|
|
def test_toggle_com(self):
|
|
self.call(comms.CmdAddCom(), "tc = testchan",
|
|
"You are already connected to channel testchan. You can now", receiver=self.account)
|
|
self.call(comms.CmdDelCom(), "tc", "Your alias 'tc' for channel testchan was cleared.", receiver=self.account)
|
|
|
|
def test_channels(self):
|
|
self.call(comms.CmdChannels(), "",
|
|
"Available channels (use comlist,addcom and delcom to manage", receiver=self.account)
|
|
|
|
def test_all_com(self):
|
|
self.call(comms.CmdAllCom(), "",
|
|
"Available channels (use comlist,addcom and delcom to manage", receiver=self.account)
|
|
|
|
def test_clock(self):
|
|
self.call(comms.CmdClock(),
|
|
"testchan=send:all()", "Lock(s) applied. Current locks on testchan:", receiver=self.account)
|
|
|
|
def test_cdesc(self):
|
|
self.call(comms.CmdCdesc(), "testchan = Test Channel",
|
|
"Description of channel 'testchan' set to 'Test Channel'.", receiver=self.account)
|
|
|
|
def test_cemit(self):
|
|
self.call(comms.CmdCemit(), "testchan = Test Message",
|
|
"[testchan] Test Message|Sent to channel testchan: Test Message", receiver=self.account)
|
|
|
|
def test_cwho(self):
|
|
self.call(comms.CmdCWho(), "testchan", "Channel subscriptions\ntestchan:\n TestAccount", receiver=self.account)
|
|
|
|
def test_page(self):
|
|
self.call(comms.CmdPage(), "TestAccount2 = Test",
|
|
"TestAccount2 is offline. They will see your message if they list their pages later."
|
|
"|You paged TestAccount2 with: 'Test'.", receiver=self.account)
|
|
|
|
def test_cboot(self):
|
|
# No one else connected to boot
|
|
self.call(comms.CmdCBoot(), "", "Usage: @cboot[/quiet] <channel> = <account> [:reason]", receiver=self.account)
|
|
|
|
def test_cdestroy(self):
|
|
self.call(comms.CmdCdestroy(), "testchan",
|
|
"[testchan] TestAccount: testchan is being destroyed. Make sure to change your aliases."
|
|
"|Channel 'testchan' was destroyed.", receiver=self.account)
|
|
|
|
|
|
class TestBatchProcess(CommandTest):
|
|
def test_batch_commands(self):
|
|
# cannot test batchcode here, it must run inside the server process
|
|
self.call(batchprocess.CmdBatchCommands(), "example_batch_cmds",
|
|
"Running Batch-command processor - Automatic mode for example_batch_cmds")
|
|
# we make sure to delete the button again here to stop the running reactor
|
|
confirm = building.CmdDestroy.confirm
|
|
building.CmdDestroy.confirm = False
|
|
self.call(building.CmdDestroy(), "button", "button was destroyed.")
|
|
building.CmdDestroy.confirm = confirm
|
|
|
|
|
|
class CmdInterrupt(Command):
|
|
|
|
key = "interrupt"
|
|
|
|
def parse(self):
|
|
raise InterruptCommand
|
|
|
|
def func(self):
|
|
self.msg("in func")
|
|
|
|
|
|
class TestInterruptCommand(CommandTest):
|
|
def test_interrupt_command(self):
|
|
ret = self.call(CmdInterrupt(), "")
|
|
self.assertEqual(ret, "")
|
|
|
|
|
|
class TestUnconnectedCommand(CommandTest):
|
|
def test_info_command(self):
|
|
# instead of using SERVER_START_TIME (0), we use 86400 because Windows won't let us use anything lower
|
|
gametime.SERVER_START_TIME = 86400
|
|
expected = "## BEGIN INFO 1.1\nName: %s\nUptime: %s\nConnected: %d\nVersion: Evennia %s\n## END INFO" % (
|
|
settings.SERVERNAME,
|
|
datetime.datetime.fromtimestamp(gametime.SERVER_START_TIME).ctime(),
|
|
SESSIONS.account_count(), utils.get_evennia_version())
|
|
self.call(unloggedin.CmdUnconnectedInfo(), "", expected)
|
|
del gametime.SERVER_START_TIME
|
|
|
|
|
|
# Test syscommands
|
|
|
|
class TestSystemCommands(CommandTest):
|
|
|
|
def test_simple_defaults(self):
|
|
self.call(syscommands.SystemNoInput(), "")
|
|
self.call(syscommands.SystemNoMatch(), "Huh?")
|
|
|
|
def test_multimatch(self):
|
|
# set up fake matches and store on command instance
|
|
cmdset = CmdSet()
|
|
cmdset.add(general.CmdLook())
|
|
cmdset.add(general.CmdLook())
|
|
matches = cmdparser.build_matches("look", cmdset)
|
|
|
|
multimatch = syscommands.SystemMultimatch()
|
|
multimatch.matches = matches
|
|
|
|
self.call(multimatch, "look", "")
|
|
|
|
@mock.patch("evennia.commands.default.syscommands.ChannelDB")
|
|
def test_channelcommand(self, mock_channeldb):
|
|
channel = mock.MagicMock()
|
|
channel.msg = mock.MagicMock()
|
|
mock_channeldb.objects.get_channel = mock.MagicMock(return_value=channel)
|
|
|
|
self.call(syscommands.SystemSendToChannel(), "public:Hello")
|
|
channel.msg.assert_called()
|