diff --git a/.travis.yml b/.travis.yml index 6473f88745..add05a621a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: python +cache: pip python: - "3.6" sudo: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b77081ced..8d3e9e4222 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,15 @@ Web/Django standard initiative (@strikaco) - Bugfixes - Fixes bug on login page where error messages were not being displayed +### Prototypes + +- `evennia.prototypes.save_prototype` now takes the prototype as a normal + argument (`prototype`) instead of having to give it as `**prototype`. +- `evennia.prototypes.search_prototype` has a new kwarg `require_single=False` that + raises a KeyError exception if query gave 0 or >1 results. +- `evennia.prototypes.spawner` can now spawn by passing a `prototype_key` + + ### Typeclasses - Add new methods on all typeclasses, useful specifically for object handling from the website/admin: @@ -81,7 +90,9 @@ Web/Django standard initiative (@strikaco) ### Utils -- Added more unit tests. +- `evennia` launcher now fully handles all django-admin commands, like running tests in parallel. +- `evennia.utils.create.account` now also takes `tags` and `attrs` keywords. +- Added many more unit tests. ### Server @@ -213,6 +224,11 @@ Web/Django standard initiative (@strikaco) - `tb_range` - Adds system for abstract positioning and movement. - Updates and some cleanup of existing contribs. + +### Internationalization + +- Polish translation by user ogotai + # Overviews ## Sept 2017: diff --git a/Dockerfile b/Dockerfile index 961d3ad8ed..94f6ce6767 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,7 +33,7 @@ LABEL maintainer="www.evennia.com" # install compilation environment RUN apk update && apk add bash gcc jpeg-dev musl-dev procps py-pip \ -py-setuptools py2-openssl python python-dev zlib-dev +py-setuptools py2-openssl python python-dev zlib-dev gettext # add the files required for pip installation COPY ./setup.py /usr/src/evennia/ diff --git a/evennia/__init__.py b/evennia/__init__.py index 50f106aefb..5d221a7ebd 100644 --- a/evennia/__init__.py +++ b/evennia/__init__.py @@ -1,10 +1,9 @@ """ Evennia MUD/MUX/MU* creation system -This is the main top-level API for Evennia. You can also explore the -evennia library by accessing evennia. directly. From -inside the game you can read docs of all object by viewing its -`__doc__` string, such as through +This is the main top-level API for Evennia. You can explore the evennia library +by accessing evennia. directly. From inside the game you can read +docs of all object by viewing its `__doc__` string, such as through @py evennia.ObjectDB.__doc__ @@ -18,6 +17,31 @@ See www.evennia.com for full documentation. """ +# docstring header + +DOCSTRING = """ +|cEvennia|n 'flat' API (use |wevennia..__doc__|n to read doc-strings + and |wdict(evennia.component)|n or + |wevennia.component.__dict__ to see contents) +|cTypeclass-bases:|n |cDatabase models|n: + DefaultAccount DefaultObject AccountDB ObjectDB + DefaultGuest DefaultCharacter ChannelDB + DefaultRoom ScriptDB + DefaultChannel DefaultExit Msg + DefaultScript +|cSearch functions:|n |cCommand parents and helpers:|n + search_account search_object default_cmds + search_script search_channel Command InterruptCommand + search_help search_message CmdSet + search_tag managers |cUtilities:|n +|cCreate functions:|n settings lockfuncs + create_account create_object logger gametime + create_script create_channel ansi spawn + create_help_entry create_message contrib managers +|cGlobal handlers:|n set_trace + TICKER_HANDLER TASK_HANDLER EvMenu EvTable + SESSION_HANDLER CHANNEL_HANDLER EvForm EvEditor """ + # Delayed loading of properties # Typeclasses @@ -114,7 +138,6 @@ def _create_version(): __version__ = _create_version() del _create_version - def _init(): """ This function is called automatically by the launcher only after @@ -188,6 +211,10 @@ def _init(): from .comms.channelhandler import CHANNEL_HANDLER from .scripts.monitorhandler import MONITOR_HANDLER + # initialize the doc string + global __doc__ + __doc__ = ansi.parse_ansi(DOCSTRING) + # API containers class _EvContainer(object): @@ -205,15 +232,17 @@ def _init(): class DBmanagers(_EvContainer): """ - Links to instantiated database managers. + Links to instantiated Django database managers. These are used + to perform more advanced custom database queries than the standard + search functions allow. - helpentry - HelpEntry.objects + helpentries - HelpEntry.objects accounts - AccountDB.objects scripts - ScriptDB.objects msgs - Msg.objects channels - Channel.objects objects - ObjectDB.objects - serverconfigs = ServerConfig.objects + serverconfigs - ServerConfig.objects tags - Tags.objects attributes - Attributes.objects diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index 46193a9106..d6d1e43f13 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -1333,19 +1333,28 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)): # list of targets - make list to disconnect from db characters = list(tar for tar in target if tar) if target else [] sessions = self.sessions.all() + if not sessions: + # no sessions, nothing to report + return "" is_su = self.is_superuser # text shown when looking in the ooc area result = ["Account |g%s|n (you are Out-of-Character)" % self.key] nsess = len(sessions) - result.append(nsess == 1 and "\n\n|wConnected session:|n" or "\n\n|wConnected sessions (%i):|n" % nsess) + result.append(nsess == 1 and + "\n\n|wConnected session:|n" or + "\n\n|wConnected sessions (%i):|n" % nsess) for isess, sess in enumerate(sessions): csessid = sess.sessid addr = "%s (%s)" % (sess.protocol_key, isinstance(sess.address, tuple) and - str(sess.address[0]) or str(sess.address)) - result.append("\n %s %s" % (session.sessid == csessid and "|w* %s|n" % (isess + 1) or - " %s" % (isess + 1), addr)) + str(sess.address[0]) or + str(sess.address)) + result.append("\n %s %s" % ( + session and + session.sessid == csessid and + "|w* %s|n" % (isess + 1) or + " %s" % (isess + 1), addr)) result.append("\n\n |whelp|n - more commands") result.append("\n |wooc |n - talk on public channel") @@ -1487,7 +1496,6 @@ class DefaultGuest(DefaultAccount): characters = self.db._playable_characters for character in characters: if character: - print("deleting Character:", character) character.delete() def at_post_disconnect(self, **kwargs): diff --git a/evennia/accounts/tests.py b/evennia/accounts/tests.py index e48b2918dd..b333aeaab7 100644 --- a/evennia/accounts/tests.py +++ b/evennia/accounts/tests.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- -from mock import Mock, MagicMock +import sys +from mock import Mock, MagicMock, patch from random import randint from unittest import TestCase @@ -60,19 +61,20 @@ class TestAccountSessionHandler(TestCase): "Check count method" self.assertEqual(self.handler.count(), len(self.handler.get())) + +@override_settings(GUEST_ENABLED=True, GUEST_LIST=["bruce_wayne"]) class TestDefaultGuest(EvenniaTest): "Check DefaultGuest class" ip = '212.216.134.22' - - def test_authenticate(self): + + @override_settings(GUEST_ENABLED=False) + def test_create_not_enabled(self): # Guest account should not be permitted account, errors = DefaultGuest.authenticate(ip=self.ip) self.assertFalse(account, 'Guest account was created despite being disabled.') - settings.GUEST_ENABLED = True - settings.GUEST_LIST = ['bruce_wayne'] - + def test_authenticate(self): # Create a guest account account, errors = DefaultGuest.authenticate(ip=self.ip) self.assertTrue(account, 'Guest account should have been created.') @@ -81,7 +83,32 @@ class TestDefaultGuest(EvenniaTest): account, errors = DefaultGuest.authenticate(ip=self.ip) self.assertFalse(account, 'Two guest accounts were created with a single entry on the guest list!') - settings.GUEST_ENABLED = False + @patch("evennia.accounts.accounts.ChannelDB.objects.get_channel") + def test_create(self, get_channel): + get_channel.connect = MagicMock(return_value=True) + account, errors = DefaultGuest.create() + self.assertTrue(account, "Guest account should have been created.") + self.assertFalse(errors) + + def test_at_post_login(self): + self.account.db._last_puppet = self.char1 + self.account.at_post_login(self.session) + self.account.at_post_login() + + def test_at_server_shutdown(self): + account, errors = DefaultGuest.create(ip=self.ip) + self.char1.delete = MagicMock() + account.db._playable_characters = [self.char1] + account.at_server_shutdown() + self.char1.delete.assert_called() + + def test_at_post_disconnect(self): + account, errors = DefaultGuest.create(ip=self.ip) + self.char1.delete = MagicMock() + account.db._playable_characters = [self.char1] + account.at_post_disconnect() + self.char1.delete.assert_called() + class TestDefaultAccountAuth(EvenniaTest): @@ -162,6 +189,7 @@ class TestDefaultAccountAuth(EvenniaTest): self.assertFalse(account.set_password('Mxyzptlk')) account.delete() + class TestDefaultAccount(TestCase): "Check DefaultAccount class" @@ -279,13 +307,83 @@ class TestAccountPuppetDeletion(EvenniaTest): @override_settings(MULTISESSION_MODE=2) def test_puppet_deletion(self): # Check for existing chars - self.assertFalse(self.account.db._playable_characters, 'Account should not have any chars by default.') + self.assertFalse(self.account.db._playable_characters, + 'Account should not have any chars by default.') # Add char1 to account's playable characters self.account.db._playable_characters.append(self.char1) - self.assertTrue(self.account.db._playable_characters, 'Char was not added to account.') + self.assertTrue(self.account.db._playable_characters, + 'Char was not added to account.') # See what happens when we delete char1. self.char1.delete() # Playable char list should be empty. - self.assertFalse(self.account.db._playable_characters, 'Playable character list is not empty! %s' % self.account.db._playable_characters) + self.assertFalse(self.account.db._playable_characters, + 'Playable character list is not empty! %s' % self.account.db._playable_characters) + + +class TestDefaultAccountEv(EvenniaTest): + """ + Testing using the EvenniaTest parent + + """ + def test_characters_property(self): + "test existence of None in _playable_characters Attr" + self.account.db._playable_characters = [self.char1, None] + chars = self.account.characters + self.assertEqual(chars, [self.char1]) + self.assertEqual(self.account.db._playable_characters, [self.char1]) + + def test_puppet_success(self): + self.account.msg = MagicMock() + with patch("evennia.accounts.accounts._MULTISESSION_MODE", 2): + self.account.puppet_object(self.session, self.char1) + self.account.msg.assert_called_with("You are already puppeting this object.") + + @patch("evennia.accounts.accounts.time.time", return_value=10000) + def test_idle_time(self, mock_time): + self.session.cmd_last_visible = 10000 - 10 + idle = self.account.idle_time + self.assertEqual(idle, 10) + + # test no sessions + with patch("evennia.accounts.accounts._SESSIONS.sessions_from_account", return_value=[]) as mock_sessh: + idle = self.account.idle_time + self.assertEqual(idle, None) + + @patch("evennia.accounts.accounts.time.time", return_value=10000) + def test_connection_time(self, mock_time): + self.session.conn_time = 10000 - 10 + conn = self.account.connection_time + self.assertEqual(conn, 10) + + # test no sessions + with patch("evennia.accounts.accounts._SESSIONS.sessions_from_account", return_value=[]) as mock_sessh: + idle = self.account.connection_time + self.assertEqual(idle, None) + + def test_create_account(self): + acct = create.account( + "TestAccount3", "test@test.com", "testpassword123", + locks="test:all()", + tags=[("tag1", "category1"), ("tag2", "category2", "data1"), ("tag3", None)], + attributes=[("key1", "value1", "category1", + "edit:false()", True), + ("key2", "value2")]) + acct.save() + self.assertTrue(acct.pk) + + def test_at_look(self): + ret = self.account.at_look() + self.assertTrue("Out-of-Character" in ret) + ret = self.account.at_look(target=self.obj1) + self.assertTrue("Obj" in ret) + ret = self.account.at_look(session=self.session) + self.assertTrue("*" in ret) # * marks session is active in list + ret = self.account.at_look(target=self.obj1, session=self.session) + self.assertTrue("Obj" in ret) + ret = self.account.at_look(target="Invalid", session=self.session) + self.assertEqual(ret, 'Invalid has no in-game appearance.') + + def test_msg(self): + self.account.msg diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index fb66eeba5a..19c2b6f3a1 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -1040,6 +1040,11 @@ class CmdLink(COMMAND_DEFAULT_CLASS): if not target: return + if target == obj: + self.caller.msg("Cannot link an object to itself.") + return + + string = "" note = "Note: %s(%s) did not have a destination set before. Make sure you linked the right thing." if not obj.destination: @@ -1123,6 +1128,7 @@ class CmdSetHome(CmdLink): Usage: @sethome [= ] + @sethom The "home" location is a "safety" location for objects; they will be moved there if their current location ceases to exist. All @@ -1162,10 +1168,10 @@ class CmdSetHome(CmdLink): old_home = obj.home obj.home = new_home if old_home: - string = "%s's home location was changed from %s(%s) to %s(%s)." % ( + string = "Home location of %s was changed from %s(%s) to %s(%s)." % ( obj, old_home, old_home.dbref, new_home, new_home.dbref) else: - string = "%s' home location was set to %s(%s)." % (obj, new_home, new_home.dbref) + string = "Home location of %s was set to %s(%s)." % (obj, new_home, new_home.dbref) self.caller.msg(string) @@ -1430,37 +1436,6 @@ def _convert_from_string(cmd, strobj): string this will always fail). """ - def rec_convert(obj): - """ - Helper function of recursive conversion calls. This is only - used for Python <=2.5. After that literal_eval is available. - """ - # simple types - try: - return int(obj) - except ValueError: - # obj cannot be converted to int - that's fine - pass - try: - return float(obj) - except ValueError: - # obj cannot be converted to float - that's fine - pass - # iterables - if obj.startswith('[') and obj.endswith(']'): - "A list. Traverse recursively." - return [rec_convert(val) for val in obj[1:-1].split(',')] - if obj.startswith('(') and obj.endswith(')'): - "A tuple. Traverse recursively." - return tuple([rec_convert(val) for val in obj[1:-1].split(',')]) - if obj.startswith('{') and obj.endswith('}') and ':' in obj: - "A dict. Traverse recursively." - return dict([(rec_convert(pair.split(":", 1)[0]), - rec_convert(pair.split(":", 1)[1])) - for pair in obj[1:-1].split(',') if ":" in pair]) - # if nothing matches, return as-is - return obj - # Use literal_eval to parse python structure exactly. try: return _LITERAL_EVAL(strobj) @@ -1471,10 +1446,9 @@ def _convert_from_string(cmd, strobj): "Make sure this is acceptable." % strobj cmd.caller.msg(string) return strobj - else: - # fall back to old recursive solution (does not support - # nested lists/dicts) - return rec_convert(strobj.strip()) + except Exception as err: + string = "|RUnknown error in evaluating Attribute: {}".format(err) + return string class CmdSetAttribute(ObjManipCommand): @@ -1957,7 +1931,7 @@ class CmdLock(ObjManipCommand): caller = self.caller if not self.args: - string = "@lock [ = ] or @lock[/switch] " \ + string = "Usage: @lock [ = ] or @lock[/switch] " \ "/" caller.msg(string) return @@ -1978,8 +1952,8 @@ class CmdLock(ObjManipCommand): caller.msg("You need 'control' access to change this type of lock.") return - if not has_control_access or obj.access(caller, "edit"): - caller.msg("You are not allowed to do that.") + if not has_control_access or not obj.access(caller, "edit"): + caller.msg("You need 'edit' access to view or delete lock on this object.") return lockdef = obj.locks.get(access_type) @@ -2753,11 +2727,11 @@ class CmdTag(COMMAND_DEFAULT_CLASS): obj) else: # no tag specified, clear all tags - old_tags = ["%s%s" % (tag, " (category: %s" % category if category else "") + old_tags = ["%s%s" % (tag, " (category: %s)" % category if category else "") for tag, category in obj.tags.all(return_key_and_category=True)] if old_tags: obj.tags.clear() - string = "Cleared all tags from %s: %s" % (obj, ", ".join(old_tags)) + string = "Cleared all tags from %s: %s" % (obj, ", ".join(sorted(old_tags))) else: string = "No Tags to clear on %s." % obj self.caller.msg(string) @@ -2788,8 +2762,9 @@ class CmdTag(COMMAND_DEFAULT_CLASS): tags = [tup[0] for tup in tagtuples] categories = [" (category: %s)" % tup[1] if tup[1] else "" for tup in tagtuples] if ntags: - string = "Tag%s on %s: %s" % ("s" if ntags > 1 else "", obj, - ", ".join("'%s'%s" % (tags[i], categories[i]) for i in range(ntags))) + string = "Tag%s on %s: %s" % ( + "s" if ntags > 1 else "", obj, + ", ".join(sorted("'%s'%s" % (tags[i], categories[i]) for i in range(ntags)))) else: string = "No tags attached to %s." % obj self.caller.msg(string) @@ -3002,7 +2977,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): # all seems ok. Try to save. try: - prot = protlib.save_prototype(**prototype) + prot = protlib.save_prototype(prototype) if not prot: caller.msg("|rError saving:|R {}.|n".format(prototype_key)) return @@ -3091,7 +3066,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): return elif nprots > 1: caller.msg("Found {} prototypes matching '{}':\n {}".format( - nprots, prototype, ", ".join(prot.get('prototype_key', '') + nprots, prototype, ", ".join(proto.get('prototype_key', '') for proto in prototypes))) return # we have a prototype, check access diff --git a/evennia/commands/default/system.py b/evennia/commands/default/system.py index 3d66f98994..0ace6592bf 100644 --- a/evennia/commands/default/system.py +++ b/evennia/commands/default/system.py @@ -7,6 +7,7 @@ System commands import traceback import os +import io import datetime import sys import django @@ -21,7 +22,7 @@ from evennia.accounts.models import AccountDB from evennia.utils import logger, utils, gametime, create, search from evennia.utils.eveditor import EvEditor from evennia.utils.evtable import EvTable -from evennia.utils.utils import crop, class_from_module +from evennia.utils.utils import crop, class_from_module, to_unicode COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) @@ -197,6 +198,7 @@ def _run_code_snippet(caller, pycode, mode="eval", measure_time=False, duration = " (runtime ~ %.4f ms)" % ((t1 - t0) * 1000) else: ret = eval(pycode_compiled, {}, available_vars) + if mode == "eval": ret = "%s%s" % (str(ret), duration) else: @@ -238,7 +240,9 @@ class CmdPy(COMMAND_DEFAULT_CLASS): inherits_from(obj, parent) : check object inheritance You can explore The evennia API from inside the game by calling - evennia.help(), evennia.managers.help() etc. + the `__doc__` property on entities: + @py evennia.__doc__ + @py evennia.managers.__doc__ |rNote: In the wrong hands this command is a severe security risk. It should only be accessible by trusted server admins/superusers.|n diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 9e35a50998..bf5d103f1c 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -11,10 +11,10 @@ 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 @@ -49,7 +49,7 @@ 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): + receiver=None, cmdstring=None, obj=None, inputs=None, raw_string=None): """ Test a command by assigning all the needed properties to cmdobj and running @@ -74,7 +74,7 @@ class CommandTest(EvenniaTest): cmdobj.cmdset = cmdset cmdobj.session = SESSIONS.session_from_sessid(1) cmdobj.account = self.account - cmdobj.raw_string = cmdobj.key + " " + args + 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 @@ -322,30 +322,90 @@ class TestBuilding(CommandTest): 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): - self.call(building.CmdSetObjAlias(), "Obj =", "Cleared aliases from Obj(#4)") + self.call(building.CmdSetObjAlias(), "Obj =", "Cleared aliases from Obj") self.call(building.CmdSetObjAlias(), "Obj = TestObj1b", "Alias(es) for 'Obj(#4)' set to 'testobj1b'.") + self.call(building.CmdSetObjAlias(), "", "Usage: ") + self.call(building.CmdSetObjAlias(), "NotFound =", "Could not find 'NotFound'.") + self.call(building.CmdSetObjAlias(), "Obj", "Aliases for Obj(#4): 'testobj1b'") + 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): self.call(building.CmdDesc(), "Obj2=TestDesc", "The description was set on Obj2(#5).") + 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): """ @@ -365,50 +425,128 @@ class TestBuilding(CommandTest): assert self.obj2.db.desc == o2d assert self.room1.db.desc == 'Obj2' and self.room1.db.desc != r1d - def test_wipe(self): + 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(), "#1", "You are trying to delete") # DEFAULT_HOME + self.char2.location = self.room2 + self.call(building.CmdDestroy(), self.room2.dbref, + "Char2(#7) arrives to Room(#1) from Room2(#2).|Room2 was destroyed.") 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(#11) (test3) of type typeclasses.rooms.Room.\n" + "Created Exit from Room to TestRoom3: east(#12) (e).\n" + "Created Exit back from TestRoom3 to Room: west(#13) (w).|TestRoom3(#11)") def test_tunnel_exit_typeclass(self): - self.call(building.CmdTunnel(), "n:evennia.objects.objects.DefaultExit = TestRoom3", "Created room TestRoom3") + 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(#8) did not have a destination set before. " "Make sure you linked the right thing.\n" "Link created TestExit1 -> Room2 (one way).") + self.call(building.CmdLink(), "TestExit1=", "Former exit TestExit1 no longer links anywhere.") def test_set_home(self): - self.call(building.CmdSetHome(), "Obj = Room2", "Obj's home location was changed from Room") + 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(), "", ":") + 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(), "Obj = test:perm(Developer)", "Added lock 'test:perm(Developer)' to Obj.") + 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 = edit:false()") + self.call(building.CmdLock(), "Obj/test", "You need 'edit' access to view or delete lock on this object.") + self.call(building.CmdLock(), "Obj", "call:true()") # etc + self.call(building.CmdLock(), "*TestAccount", "boot:perm(Admin)") # etc def test_find(self): + self.call(building.CmdFind(), "", "Usage: ") self.call(building.CmdFind(), "oom2", "One Match") + self.call(building.CmdFind(), "oom2 = 1-100", "One Match") + self.call(building.CmdFind(), "oom2 = 1 100", "One Match") # space works too expect = "One Match(#1-#7, loc):\n " +\ "Char2(#7) - evennia.objects.objects.DefaultCharacter (location: Room(#1))" self.call(building.CmdFind(), "Char2", expect, cmdstring="locate") @@ -420,11 +558,43 @@ class TestBuilding(CommandTest): 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(#1-#7):\n " + " Char(#6) - evennia.objects.objects.DefaultCharacter") + self.call(building.CmdFind(), "*TestAccount", + "Match(#1-#7):\n" + " TestAccount - evennia.accounts.accounts.DefaultAccount") + + 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 .") + 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): - self.call(building.CmdTeleport(), "/quiet Room2", "Room2(#2)\n|Teleported to Room2.") + 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(#4) is leaving Room(#1), heading for Room2(#2).|Teleported Obj -> Room2.") + 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(#2)") self.call(building.CmdTeleport(), "/t", # /t switch is abbreviated form of /tonone "Cannot teleport a puppeted object (Char, puppeted by TestAccount(account 1)) to a None-location.") self.call(building.CmdTeleport(), "/l Room2", # /l switch is abbreviated form of /loc @@ -432,6 +602,28 @@ class TestBuilding(CommandTest): 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 @@ -453,6 +645,14 @@ class TestBuilding(CommandTest): "'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") @@ -480,9 +680,9 @@ class TestBuilding(CommandTest): goblin.delete() # create prototype - protlib.create_prototype(**{'key': 'Ball', - 'typeclass': 'evennia.objects.objects.DefaultCharacter', - 'prototype_key': 'testball'}) + protlib.create_prototype({'key': 'Ball', + 'typeclass': 'evennia.objects.objects.DefaultCharacter', + 'prototype_key': 'testball'}) # Tests "@spawn " self.call(building.CmdSpawn(), "testball", "Spawned Ball") diff --git a/evennia/contrib/health_bar.py b/evennia/contrib/health_bar.py index c3d4af1c52..5ccfa1acbd 100644 --- a/evennia/contrib/health_bar.py +++ b/evennia/contrib/health_bar.py @@ -90,7 +90,7 @@ def display_meter(cur_value, max_value, # Pick which fill color to use based on how full the bar is fillcolor_index = (float(len(fill_color)) * percent_full) - fillcolor_index = int(round(fillcolor_index)) - 1 + fillcolor_index = max(0, int(round(fillcolor_index)) - 1) fillcolor_code = "|[" + fill_color[fillcolor_index] # Make color codes for empty bar portion and text_color diff --git a/evennia/contrib/security/auditing/tests.py b/evennia/contrib/security/auditing/tests.py index bd825701c2..e6da244188 100644 --- a/evennia/contrib/security/auditing/tests.py +++ b/evennia/contrib/security/auditing/tests.py @@ -2,17 +2,18 @@ Module containing the test cases for the Audit system. """ +from django.test import override_settings from django.conf import settings from evennia.utils.test_resources import EvenniaTest import re -# Configure session auditing settings +# Configure session auditing settings - TODO: This is bad practice that leaks over to other tests settings.AUDIT_CALLBACK = "evennia.security.contrib.auditing.outputs.to_syslog" settings.AUDIT_IN = True settings.AUDIT_OUT = True settings.AUDIT_ALLOW_SPARSE = True -# Configure settings to use custom session +# Configure settings to use custom session - TODO: This is bad practice, changing global settings settings.SERVER_SESSION_CLASS = "evennia.contrib.security.auditing.server.AuditedServerSession" diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 51824e1b3f..ff6b1b7767 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -691,8 +691,21 @@ from evennia.contrib import health_bar class TestHealthBar(EvenniaTest): def test_healthbar(self): - expected_bar_str = "|[G|w |n|[B|w test24 / 200test |n" - self.assertTrue(health_bar.display_meter(24, 200, length=40, pre_text="test", post_text="test", align="center") == expected_bar_str) + expected_bar_str = '|[R|w|n|[B|w test0 / 200test |n' + self.assertEqual(health_bar.display_meter( + 0, 200, length=40, pre_text="test", post_text="test", align="center"), expected_bar_str) + expected_bar_str = "|[R|w |n|[B|w test24 / 200test |n" + self.assertEqual(health_bar.display_meter( + 24, 200, length=40, pre_text="test", post_text="test", align="center"), expected_bar_str) + expected_bar_str = '|[Y|w test100 /|n|[B|w 200test |n' + self.assertEqual(health_bar.display_meter( + 100, 200, length=40, pre_text="test", post_text="test", align="center"), expected_bar_str) + expected_bar_str = '|[G|w test180 / 200test |n|[B|w |n' + self.assertEqual(health_bar.display_meter( + 180, 200, length=40, pre_text="test", post_text="test", align="center"), expected_bar_str) + expected_bar_str = '|[G|w test200 / 200test |n|[B|w|n' + self.assertEqual(health_bar.display_meter( + 200, 200, length=40, pre_text="test", post_text="test", align="center"), expected_bar_str) # test mail contrib diff --git a/evennia/contrib/tutorial_world/build.ev b/evennia/contrib/tutorial_world/build.ev index ce1bbe4bcf..df454ad937 100644 --- a/evennia/contrib/tutorial_world/build.ev +++ b/evennia/contrib/tutorial_world/build.ev @@ -177,7 +177,7 @@ start # # This room inherits from a Typeclass called WeatherRoom. It regularly # and randomly shows some weather effects. Note how we can spread the -# command's arguments over more than one line for easy reading. we +# command's arguments over more than one line for easy reading. We # also make sure to create plenty of aliases for the room and # exits. Note the alias tut#02: this unique identifier can be used # later in the script to always find the way back to this room (for @@ -420,10 +420,6 @@ north (to get a weapon from the barrel, use |wget weapon|n) # -@desc barrel = - This barrel has the air of leftovers - it contains an assorted - mess of random weaponry in various states and qualities. -# @detail barkeep;man;landlord = The landlord is a cheerful fellow, always ready to supply you with more beer. He mentions doing some sort of arcane magic known as @@ -439,8 +435,17 @@ north # @create/drop barrel: tutorial_world.objects.WeaponRack # +@desc barrel = + This barrel has the air of leftovers - it contains an assorted + mess of random weaponry in various states and qualities. +# @lock barrel = get:false() # +# Players trying to pickup barrel will receive hint to 'get weapon' instead +# +@set barrel/get_err_msg = + The barkeep shakes his head. He says: 'Get weapon, not the barrel.' +# # This id makes sure that we cannot pick more than one weapon from this rack # @set barrel/rack_id = "rack_barrel" @@ -836,7 +841,7 @@ archway # # Set its home to this location # -@home ghost = tut#11 +@sethome ghost = tut#11 # @lock ghost = get:false() # diff --git a/evennia/contrib/tutorial_world/mob.py b/evennia/contrib/tutorial_world/mob.py index 8a27c7cf51..b87640189f 100644 --- a/evennia/contrib/tutorial_world/mob.py +++ b/evennia/contrib/tutorial_world/mob.py @@ -37,7 +37,7 @@ class CmdMobOnOff(Command): to turn on/off the mob." """ if not self.args: - self.caller.msg("Usage: mobon|moboff ") + self.caller.msg("Usage: mobon||moboff ") return mob = self.caller.search(self.args) if not mob: diff --git a/evennia/locale/pl/LC_MESSAGES/django.mo b/evennia/locale/pl/LC_MESSAGES/django.mo new file mode 100644 index 0000000000..021e81ea31 Binary files /dev/null and b/evennia/locale/pl/LC_MESSAGES/django.mo differ diff --git a/evennia/locale/pl/LC_MESSAGES/django.po b/evennia/locale/pl/LC_MESSAGES/django.po new file mode 100644 index 0000000000..61ebe5cd63 --- /dev/null +++ b/evennia/locale/pl/LC_MESSAGES/django.po @@ -0,0 +1,315 @@ +# Polish localization for the EVENNIA. +# Copyright (C) 2019 +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , 2019. +# +msgid "" +msgstr "" +"Project-Id-Version: ArkMUD Polish translation v0.1\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-02-20 12:13+0000\n" +"PO-Revision-Date: 2019-02-20 14:18+0100\n" +"Language: pl_PL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n" +"%100<12 || n%100>=14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n" +"%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" +"Last-Translator: \n" +"Language-Team: ArkMUD team\n" +"X-Generator: Poedit 2.2.1\n" + +#: accounts/accounts.py:440 +msgid "Account being deleted." +msgstr "Konto zostalo usuniete." + +#: commands/cmdhandler.py:681 +msgid "There were multiple matches." +msgstr "Znaleziono wiele dopasowan." + +#: commands/cmdhandler.py:704 +#, python-format +msgid "Command '%s' is not available." +msgstr "Komenda '%s' jests niedostepna." + +#: commands/cmdhandler.py:709 +#, python-format +msgid " Maybe you meant %s?" +msgstr " Czy miales na mysli %s?" + +#: commands/cmdhandler.py:709 +msgid "or" +msgstr "lub" + +#: commands/cmdhandler.py:711 +msgid " Type \"help\" for help." +msgstr " Wpisz \"help\" aby otworzyc pomoc." + +#: commands/cmdsethandler.py:89 +#, python-brace-format +msgid "" +"{traceback}\n" +"Error loading cmdset '{path}'\n" +"(Traceback was logged {timestamp})" +msgstr "" + +#: commands/cmdsethandler.py:94 +#, python-brace-format +msgid "" +"Error loading cmdset: No cmdset class '{classname}' in '{path}'.\n" +"(Traceback was logged {timestamp})" +msgstr "" + +#: commands/cmdsethandler.py:98 +#, python-brace-format +msgid "" +"{traceback}\n" +"SyntaxError encountered when loading cmdset '{path}'.\n" +"(Traceback was logged {timestamp})" +msgstr "" + +#: commands/cmdsethandler.py:103 +#, python-brace-format +msgid "" +"{traceback}\n" +"Compile/Run error when loading cmdset '{path}'.\",\n" +"(Traceback was logged {timestamp})" +msgstr "" + +#: commands/cmdsethandler.py:108 +#, python-brace-format +msgid "" +"\n" +"Error encountered for cmdset at path '{path}'.\n" +"Replacing with fallback '{fallback_path}'.\n" +msgstr "" + +#: commands/cmdsethandler.py:114 +#, python-brace-format +msgid "Fallback path '{fallback_path}' failed to generate a cmdset." +msgstr "" + +#: commands/cmdsethandler.py:182 commands/cmdsethandler.py:192 +#, python-format +msgid "" +"\n" +"(Unsuccessfully tried '%s')." +msgstr "" +"\n" +"(Bezskuteczna proba '%s')." + +#: commands/cmdsethandler.py:311 +#, python-brace-format +msgid "custom {mergetype} on cmdset '{cmdset}'" +msgstr "" + +#: commands/cmdsethandler.py:314 +#, python-brace-format +msgid " : {current}" +msgstr "" + +#: commands/cmdsethandler.py:322 +#, python-brace-format +msgid "" +" <{key} ({mergetype}, prio {prio}, {permstring})>:\n" +" {keylist}" +msgstr "" + +#: commands/cmdsethandler.py:426 +msgid "Only CmdSets can be added to the cmdsethandler!" +msgstr "" + +#: comms/channelhandler.py:100 +msgid "Say what?" +msgstr "Ze co?" + +#: comms/channelhandler.py:105 +#, python-format +msgid "Channel '%s' not found." +msgstr "Kanal '%s' nie odnaleziony." + +#: comms/channelhandler.py:108 +#, python-format +msgid "You are not connected to channel '%s'." +msgstr "Nie jestes polaczony z kanalem '%s'." + +#: comms/channelhandler.py:112 +#, python-format +msgid "You are not permitted to send to channel '%s'." +msgstr "Nie masz uprawnien do wysylania wiadomosci na kanal '%s'." + +#: comms/channelhandler.py:155 +msgid " (channel)" +msgstr " (kanal)" + +#: locks/lockhandler.py:236 +#, python-format +msgid "Lock: lock-function '%s' is not available." +msgstr "" + +#: locks/lockhandler.py:249 +#, python-format +msgid "Lock: definition '%s' has syntax errors." +msgstr "" + +#: locks/lockhandler.py:253 +#, python-format +msgid "" +"LockHandler on %(obj)s: access type '%(access_type)s' changed from " +"'%(source)s' to '%(goal)s' " +msgstr "" + +#: locks/lockhandler.py:320 +#, python-brace-format +msgid "Lock: '{lockdef}' contains no colon (:)." +msgstr "" + +#: locks/lockhandler.py:328 +#, python-brace-format +msgid "Lock: '{lockdef}' has no access_type (left-side of colon is empty)." +msgstr "" + +#: locks/lockhandler.py:336 +#, python-brace-format +msgid "Lock: '{lockdef}' has mismatched parentheses." +msgstr "" + +#: locks/lockhandler.py:343 +#, python-brace-format +msgid "Lock: '{lockdef}' has no valid lock functions." +msgstr "" + +#: objects/objects.py:732 +#, python-format +msgid "Couldn't perform move ('%s'). Contact an admin." +msgstr "Nie udalo sie wykonac ruchu ('%s'). Skontaktuj sie z adminem." + +#: objects/objects.py:742 +msgid "The destination doesn't exist." +msgstr "Punkt przeznaczenia nie istnieje." + +#: objects/objects.py:833 +#, python-format +msgid "Could not find default home '(#%d)'." +msgstr "Nie znaleziono domyslnego domu '(#%d)'." + +#: objects/objects.py:849 +msgid "Something went wrong! You are dumped into nowhere. Contact an admin." +msgstr "Cos poszlo zle! Zostales wrzucony w nicosc. Skontaktuj sie z adminem." + +#: objects/objects.py:915 +#, python-format +msgid "Your character %s has been destroyed." +msgstr "Twoja postac %s zostala zniszczona." + +#: scripts/scripthandler.py:53 +#, python-format +msgid "" +"\n" +" '%(key)s' (%(next_repeat)s/%(interval)s, %(repeats)s repeats): %(desc)s" +msgstr "" + +#: scripts/scripts.py:205 +#, python-format +msgid "" +"Script %(key)s(#%(dbid)s) of type '%(cname)s': at_repeat() error '%(err)s'." +msgstr "" + +#: server/initial_setup.py:28 +msgid "" +"\n" +"Welcome to your new |wEvennia|n-based game! Visit http://www.evennia.com if " +"you need\n" +"help, want to contribute, report issues or just join the community.\n" +"As Account #1 you can create a demo/tutorial area with |w@batchcommand " +"tutorial_world.build|n.\n" +" " +msgstr "" +"\n" +"Witaj w swojej nowej grze, bazujacej na |wEvennia|n! Odwiedz http://www." +"evennia.com\n" +"jesli potrzebujesz pomocy, chcesz pomoc badz zglosic blad, lub po prostu " +"chcesz dolaczyc do spolecznosci.\n" +"Jako Konto #1 mozesz otworzyc demo/samouczek wpisujac |w@batchcommand " +"tutorial_world.build|n.\n" +" " + +#: server/initial_setup.py:92 +msgid "This is User #1." +msgstr "To jest User #1." + +#: server/initial_setup.py:105 +msgid "Limbo" +msgstr "Otchlan" + +#: server/server.py:139 +msgid "idle timeout exceeded" +msgstr "czas bezczynnosci przekroczony" + +#: server/sessionhandler.py:386 +msgid " ... Server restarted." +msgstr " ... Serwer zrestartowany." + +#: server/sessionhandler.py:606 +msgid "Logged in from elsewhere. Disconnecting." +msgstr "Zalogowano z innego miejsca. Rozlaczanie." + +#: server/sessionhandler.py:634 +msgid "Idle timeout exceeded, disconnecting." +msgstr "Czas bezczynnosci przekroczony, rozlaczanie." + +#: server/validators.py:50 +#, python-format +msgid "" +"%s From a terminal client, you can also use a phrase of multiple words if " +"you enclose the password in double quotes." +msgstr "" +"%s Z poziomu terminala, mozesz rowniez uzyc frazy z wieloma slowami jesli " +"ujmiesz haslo w cudzyslowie." + +#: utils/evmenu.py:192 +#, python-brace-format +msgid "" +"Menu node '{nodename}' is either not implemented or caused an error. Make " +"another choice." +msgstr "" + +#: utils/evmenu.py:194 +#, python-brace-format +msgid "Error in menu node '{nodename}'." +msgstr "" + +#: utils/evmenu.py:195 +msgid "No description." +msgstr "Brak opisu." + +#: utils/evmenu.py:196 +msgid "Commands: , help, quit" +msgstr "" + +#: utils/evmenu.py:197 +msgid "Commands: , help" +msgstr "" + +#: utils/evmenu.py:198 +msgid "Commands: help, quit" +msgstr "" + +#: utils/evmenu.py:199 +msgid "Commands: help" +msgstr "" + +#: utils/evmenu.py:200 +msgid "Choose an option or try 'help'." +msgstr "Wybierz opcje lub uzyj komendy 'help'." + +#: utils/utils.py:1866 +#, python-format +msgid "Could not find '%s'." +msgstr "Nie odnaleziono '%s'." + +#: utils/utils.py:1873 +#, python-format +msgid "More than one match for '%s' (please narrow target):\n" +msgstr "Wiecej niz jedno dopasowanie dla '%s' (prosze zawezyc cel):\n" diff --git a/evennia/objects/manager.py b/evennia/objects/manager.py index c8ae07afa4..ba1cc9fa8d 100644 --- a/evennia/objects/manager.py +++ b/evennia/objects/manager.py @@ -69,7 +69,7 @@ class ObjectDBManager(TypedObjectManager): object candidates. Return: - match (Object or list): One or more matching results. + match (query): Matching query. """ ostring = str(ostring).lstrip('*') @@ -77,7 +77,7 @@ class ObjectDBManager(TypedObjectManager): dbref = self.dbref(ostring) if dbref: try: - return self.get(id=dbref) + return self.get(db_account__id=dbref) except self.model.DoesNotExist: pass @@ -87,13 +87,13 @@ class ObjectDBManager(TypedObjectManager): if exact: return self.filter(cand_restriction & Q(db_account__username__iexact=ostring)) else: # fuzzy matching - ply_cands = self.filter(cand_restriction & Q(accountdb__username__istartswith=ostring) - ).values_list("db_key", flat=True) - if candidates: - index_matches = string_partial_matching(ply_cands, ostring, ret_index=True) - return [obj for ind, obj in enumerate(make_iter(candidates)) if ind in index_matches] - else: - return string_partial_matching(ply_cands, ostring, ret_index=False) + obj_cands = self.select_related().filter(cand_restriction & Q(db_account__username__istartswith=ostring)) + acct_cands = [obj.account for obj in obj_cands] + + if obj_cands: + index_matches = string_partial_matching([acct.key for acct in acct_cands], ostring, ret_index=True) + acct_cands = [acct_cands[i].id for i in index_matches] + return obj_cands.filter(db_account__id__in=acct_cands) def get_objs_with_key_and_typeclass(self, oname, otypeclass_path, candidates=None): """ @@ -105,7 +105,7 @@ class ObjectDBManager(TypedObjectManager): candidates (list, optional): Only match among the given list of candidates. Returns: - matches (list): The matching objects. + matches (query): The matching objects. """ cand_restriction = candidates is not None and Q(pk__in=[_GA(obj, "id") for obj in make_iter(candidates) if obj]) or Q() @@ -119,18 +119,19 @@ class ObjectDBManager(TypedObjectManager): Args: attribute_name (str): Attribute name to search for. - candidates (list, optional): Only match among the given list of candidates. + candidates (list, optional): Only match among the given list of object + candidates. Returns: - matches (list): All objects having the given attribute_name defined at all. + matches (query): All objects having the given attribute_name defined at all. """ - cand_restriction = candidates is not None and Q(db_attributes__db_obj__pk__in=[_GA(obj, "id") for obj - in make_iter(candidates) - if obj]) or Q() - return list(self.filter(cand_restriction & Q(db_attributes__db_key=attribute_name))) + cand_restriction = \ + candidates is not None and Q(id__in=[obj.id for obj in candidates]) or Q() + return self.filter(cand_restriction & Q(db_attributes__db_key=attribute_name)) - def get_objs_with_attr_value(self, attribute_name, attribute_value, candidates=None, typeclasses=None): + def get_objs_with_attr_value(self, attribute_name, attribute_value, + candidates=None, typeclasses=None): """ Get all objects having the given attrname set to the given value. @@ -141,7 +142,8 @@ class ObjectDBManager(TypedObjectManager): typeclasses (list, optional): Python pats to restrict matches with. Returns: - matches (list): Objects fullfilling both the `attribute_name` and `attribute_value` criterions. + matches (list): Objects fullfilling both the `attribute_name` and + `attribute_value` criterions. Notes: This uses the Attribute's PickledField to transparently search the database by matching diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 3d8ed740a5..cabcdccfe2 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -262,7 +262,13 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): con = self.contents_cache.get(exclude=exclude) # print "contents_get:", self, con, id(self), calledby() # DEBUG return con - contents = property(contents_get) + + def contents_set(self, *args): + "You cannot replace this property" + raise AttributeError("{}.contents is read-only. Use obj.move_to or " + "obj.location to move an object here.".format(self.__class__)) + + contents = property(contents_get, contents_set, contents_set) @property def exits(self): diff --git a/evennia/objects/tests.py b/evennia/objects/tests.py index 6bfe34248f..8098f55b2f 100644 --- a/evennia/objects/tests.py +++ b/evennia/objects/tests.py @@ -1,5 +1,6 @@ from evennia.utils.test_resources import EvenniaTest from evennia import DefaultObject, DefaultCharacter, DefaultRoom, DefaultExit +from evennia.objects.models import ObjectDB class DefaultObjectTest(EvenniaTest): @@ -45,3 +46,48 @@ class DefaultObjectTest(EvenniaTest): self.assertTrue(self.room1.get_absolute_url()) self.assertTrue('admin' in self.room1.web_get_admin_url()) + + +class TestObjectManager(EvenniaTest): + "Test object manager methods" + def test_get_object_with_account(self): + query = ObjectDB.objects.get_object_with_account("TestAccount").first() + self.assertEqual(query, self.char1) + query = ObjectDB.objects.get_object_with_account(self.account.dbref) + self.assertEqual(query, self.char1) + query = ObjectDB.objects.get_object_with_account("#123456") + self.assertFalse(query) + query = ObjectDB.objects.get_object_with_account("TestAccou").first() + self.assertFalse(query) + + query = ObjectDB.objects.get_object_with_account("TestAccou", exact=False) + self.assertEqual(tuple(query), (self.char1, self.char2)) + + query = ObjectDB.objects.get_object_with_account( + "TestAccou", candidates=[self.char1, self.obj1], exact=False) + self.assertEqual(list(query), [self.char1]) + + def test_get_objs_with_key_and_typeclass(self): + query = ObjectDB.objects.get_objs_with_key_and_typeclass( + "Char", "evennia.objects.objects.DefaultCharacter") + self.assertEqual(list(query), [self.char1]) + query = ObjectDB.objects.get_objs_with_key_and_typeclass( + "Char", "evennia.objects.objects.DefaultObject") + self.assertFalse(query) + query = ObjectDB.objects.get_objs_with_key_and_typeclass( + "NotFound", "evennia.objects.objects.DefaultCharacter") + self.assertFalse(query) + query = ObjectDB.objects.get_objs_with_key_and_typeclass( + "Char", "evennia.objects.objects.DefaultCharacter", candidates=[self.char1, self.char2]) + self.assertEqual(list(query), [self.char1]) + + def test_get_objs_with_attr(self): + self.obj1.db.testattr = "testval1" + query = ObjectDB.objects.get_objs_with_attr("testattr") + self.assertEqual(list(query), [self.obj1]) + query = ObjectDB.objects.get_objs_with_attr( + "testattr", candidates=[self.char1, self.obj1] ) + self.assertEqual(list(query), [self.obj1]) + query = ObjectDB.objects.get_objs_with_attr( + "NotFound", candidates=[self.char1, self.obj1] ) + self.assertFalse(query) diff --git a/evennia/prototypes/README.md b/evennia/prototypes/README.md index 0f4139aa3e..2cca210ae1 100644 --- a/evennia/prototypes/README.md +++ b/evennia/prototypes/README.md @@ -76,7 +76,7 @@ from evennia import prototypes goblin = {"prototype_key": "goblin:, ... } -prototype = prototypes.save_prototype(caller, **goblin) +prototype = prototypes.save_prototype(goblin) ``` diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index 35c84f61bc..7a2156e4ec 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -2139,7 +2139,7 @@ def node_prototype_save(caller, **kwargs): # we already validated and accepted the save, so this node acts as a goto callback and # should now only return the next node prototype_key = prototype.get("prototype_key") - protlib.save_prototype(**prototype) + protlib.save_prototype(prototype) spawned_objects = protlib.search_objects_with_prototype(prototype_key) nspawned = spawned_objects.count() diff --git a/evennia/prototypes/protfuncs.py b/evennia/prototypes/protfuncs.py index 478967722b..4290bf6bdf 100644 --- a/evennia/prototypes/protfuncs.py +++ b/evennia/prototypes/protfuncs.py @@ -333,7 +333,7 @@ def objlist(*args, **kwargs): def dbref(*args, **kwargs): """ Usage $dbref(<#dbref>) - Returns one Object searched globally by #dbref. Error if #dbref is invalid. + Validate that a #dbref input is valid. """ if not args or len(args) < 1 or _RE_DBREF.match(args[0]) is None: raise ValueError('$dbref requires a valid #dbref argument.') diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index ae4df3feca..c0e5088deb 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -147,13 +147,13 @@ class DbPrototype(DefaultScript): # Prototype manager functions -def save_prototype(**kwargs): +def save_prototype(prototype): """ Create/Store a prototype persistently. - Kwargs: - prototype_key (str): This is required for any storage. - All other kwargs are considered part of the new prototype dict. + Args: + prototype (dict): The prototype to save. A `prototype_key` key is + required. Returns: prototype (dict or None): The prototype stored using the given kwargs, None if deleting. @@ -166,8 +166,8 @@ def save_prototype(**kwargs): is expected to have valid permissions. """ - - kwargs = homogenize_prototype(kwargs) + in_prototype = prototype + in_prototype = homogenize_prototype(in_prototype) def _to_batchtuple(inp, *args): "build tuple suitable for batch-creation" @@ -176,7 +176,7 @@ def save_prototype(**kwargs): return inp return (inp, ) + args - prototype_key = kwargs.get("prototype_key") + prototype_key = in_prototype.get("prototype_key") if not prototype_key: raise ValidationError("Prototype requires a prototype_key") @@ -192,21 +192,21 @@ def save_prototype(**kwargs): stored_prototype = DbPrototype.objects.filter(db_key=prototype_key) prototype = stored_prototype[0].prototype if stored_prototype else {} - kwargs['prototype_desc'] = kwargs.get("prototype_desc", prototype.get("prototype_desc", "")) - prototype_locks = kwargs.get( + in_prototype['prototype_desc'] = in_prototype.get("prototype_desc", prototype.get("prototype_desc", "")) + prototype_locks = in_prototype.get( "prototype_locks", prototype.get('prototype_locks', "spawn:all();edit:perm(Admin)")) is_valid, err = validate_lockstring(prototype_locks) if not is_valid: raise ValidationError("Lock error: {}".format(err)) - kwargs['prototype_locks'] = prototype_locks + in_prototype['prototype_locks'] = prototype_locks prototype_tags = [ _to_batchtuple(tag, _PROTOTYPE_TAG_META_CATEGORY) - for tag in make_iter(kwargs.get("prototype_tags", + for tag in make_iter(in_prototype.get("prototype_tags", prototype.get('prototype_tags', [])))] - kwargs["prototype_tags"] = prototype_tags + in_prototype["prototype_tags"] = prototype_tags - prototype.update(kwargs) + prototype.update(in_prototype) if stored_prototype: # edit existing prototype @@ -261,19 +261,25 @@ def delete_prototype(prototype_key, caller=None): return True -def search_prototype(key=None, tags=None): +def search_prototype(key=None, tags=None, require_single=False): """ Find prototypes based on key and/or tags, or all prototypes. Kwargs: key (str): An exact or partial key to query for. - tags (str or list): Tag key or keys to query for. These + tags (str or list): Tag key or keys to query for. These will always be applied with the 'db_protototype' tag category. + require_single (bool): If set, raise KeyError if the result + was not found or if there are multiple matches. Return: - matches (list): All found prototype dicts. If no keys - or tags are given, all available prototypes will be returned. + matches (list): All found prototype dicts. Empty list if + no match was found. Note that if neither `key` nor `tags` + were given, *all* available prototypes will be returned. + + Raises: + KeyError: If `require_single` is True and there are 0 or >1 matches. Note: The available prototypes is a combination of those supplied in @@ -329,6 +335,10 @@ def search_prototype(key=None, tags=None): if mta.get('prototype_key') and mta['prototype_key'] == key] if filter_matches and len(filter_matches) < nmatches: matches = filter_matches + + nmatches = len(matches) + if nmatches != 1 and require_single: + raise KeyError("Found {} matching prototypes.".format(nmatches)) return matches diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index 49ae8fe6c9..2ab6be5ee9 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -23,7 +23,7 @@ prot = { "attrs": [("weapon", "sword")] } -prot = prototypes.create_prototype(**prot) +prot = prototypes.create_prototype(prot) ``` @@ -662,8 +662,9 @@ def spawn(*prototypes, **kwargs): Spawn a number of prototyped objects. Args: - prototypes (dict): Each argument should be a prototype - dictionary. + prototypes (str or dict): Each argument should either be a + prototype_key (will be used to find the prototype) or a full prototype + dictionary. These will be batched-spawned as one object each. Kwargs: prototype_modules (str or list): A python-path to a prototype module, or a list of such paths. These will be used to build @@ -673,8 +674,10 @@ def spawn(*prototypes, **kwargs): prototype_parents (dict): A dictionary holding a custom prototype-parent dictionary. Will overload same-named prototypes from prototype_modules. - return_parents (bool): Only return a dict of the - prototype-parents (no object creation happens) + return_parents (bool): Return a dict of the entire prototype-parent tree + available to this prototype (no object creation happens). This is a + merged result between the globally found protparents and whatever + custom `prototype_parents` are given to this function. only_validate (bool): Only run validation of prototype/parents (no object creation) and return the create-kwargs. @@ -684,6 +687,11 @@ def spawn(*prototypes, **kwargs): `return_parents` is set, instead return dict of prototype parents. """ + # search string (=prototype_key) from input + prototypes = [protlib.search_prototype(prot, require_single=True)[0] + if isinstance(prot, basestring) else prot + for prot in prototypes] + # get available protparents protparents = {prot['prototype_key'].lower(): prot for prot in protlib.search_prototype()} diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index fc0ec2f8b1..9c3bbefe37 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -54,7 +54,7 @@ class TestSpawner(EvenniaTest): self.prot1 = {"prototype_key": "testprototype", "typeclass": "evennia.objects.objects.DefaultObject"} - def test_spawn(self): + def test_spawn_from_prot(self): obj1 = spawner.spawn(self.prot1) # check spawned objects have the right tag self.assertEqual(list(protlib.search_objects_with_prototype("testprototype")), obj1) @@ -62,6 +62,14 @@ class TestSpawner(EvenniaTest): _PROTPARENTS["GOBLIN"], _PROTPARENTS["GOBLIN_ARCHWIZARD"], prototype_parents=_PROTPARENTS)], ['goblin grunt', 'goblin archwizard']) + def test_spawn_from_str(self): + protlib.save_prototype(self.prot1) + obj1 = spawner.spawn(self.prot1['prototype_key']) + self.assertEqual(list(protlib.search_objects_with_prototype("testprototype")), obj1) + self.assertEqual([o.key for o in spawner.spawn( + _PROTPARENTS["GOBLIN"], _PROTPARENTS["GOBLIN_ARCHWIZARD"], + prototype_parents=_PROTPARENTS)], ['goblin grunt', 'goblin archwizard']) + class TestUtils(EvenniaTest): @@ -245,6 +253,7 @@ class TestProtLib(EvenniaTest): super(TestProtLib, self).setUp() self.obj1.attributes.add("testattr", "testval") self.prot = spawner.prototype_from_object(self.obj1) + def test_prototype_to_str(self): prstr = protlib.prototype_to_str(self.prot) @@ -253,6 +262,22 @@ class TestProtLib(EvenniaTest): def test_check_permission(self): pass + def test_save_prototype(self): + result = protlib.save_prototype(self.prot) + self.assertEqual(result, self.prot) + # faulty + self.prot['prototype_key'] = None + self.assertRaises(protlib.ValidationError, protlib.save_prototype, self.prot) + + def test_search_prototype(self): + protlib.save_prototype(self.prot) + match = protlib.search_prototype("NotFound") + self.assertFalse(match) + match = protlib.search_prototype() + self.assertTrue(match) + match = protlib.search_prototype(self.prot['prototype_key']) + self.assertEqual(match, [self.prot]) + @override_settings(PROT_FUNC_MODULES=['evennia.prototypes.protfuncs'], CLIENT_DEFAULT_WIDTH=20) class TestProtFuncs(EvenniaTest): @@ -424,7 +449,7 @@ class TestPrototypeStorage(EvenniaTest): def test_prototype_storage(self): # from evennia import set_trace;set_trace(term_size=(180, 50)) - prot1 = protlib.create_prototype(**self.prot1) + prot1 = protlib.create_prototype(self.prot1) self.assertTrue(bool(prot1)) self.assertEqual(prot1, self.prot1) @@ -436,7 +461,7 @@ class TestPrototypeStorage(EvenniaTest): protlib.DbPrototype.objects.get_by_tag( "foo1", _PROTOTYPE_TAG_META_CATEGORY)[0].db.prototype, prot1) - prot2 = protlib.create_prototype(**self.prot2) + prot2 = protlib.create_prototype(self.prot2) self.assertEqual( [pobj.db.prototype for pobj in protlib.DbPrototype.objects.get_by_tag( @@ -445,7 +470,7 @@ class TestPrototypeStorage(EvenniaTest): # add to existing prototype prot1b = protlib.create_prototype( - prototype_key='testprototype1', foo='bar', prototype_tags=['foo2']) + {"prototype_key": 'testprototype1', "foo": 'bar', "prototype_tags": ['foo2']}) self.assertEqual( [pobj.db.prototype @@ -457,7 +482,7 @@ class TestPrototypeStorage(EvenniaTest): self.assertNotEqual(list(protlib.search_prototype("testprototype1")), [prot1]) self.assertEqual(list(protlib.search_prototype("testprototype1")), [prot1b]) - prot3 = protlib.create_prototype(**self.prot3) + prot3 = protlib.create_prototype(self.prot3) # partial match with mock.patch("evennia.prototypes.prototypes._MODULE_PROTOTYPES", {}): @@ -608,7 +633,7 @@ class TestMenuModule(EvenniaTest): self.assertEqual(olc_menus._display_tag(olc_menus._get_menu_prototype(caller)['tags'][0]), Something) self.assertEqual(olc_menus._caller_tags(caller), ["foo2", "foo3"]) - protlib.save_prototype(**self.test_prot) + protlib.save_prototype(self.test_prot) # locks helpers self.assertEqual(olc_menus._lock_add(caller, "foo:false()"), "Added lock 'foo:false()'.") diff --git a/evennia/server/evennia_launcher.py b/evennia/server/evennia_launcher.py index 7e1a583d94..e7619fbce4 100644 --- a/evennia/server/evennia_launcher.py +++ b/evennia/server/evennia_launcher.py @@ -13,6 +13,7 @@ Run the script with the -h flag to see usage information. import os import sys +import re import signal import shutil import importlib @@ -25,6 +26,7 @@ from subprocess import Popen, check_output, call, CalledProcessError, STDOUT from twisted.protocols import amp from twisted.internet import reactor, endpoints import django +from django.core.management import execute_from_command_line # Signal processing SIG = signal.SIGINT @@ -393,6 +395,7 @@ ERROR_DJANGO_MIN = \ ERROR: Django {dversion} found. Evennia requires version {django_min} or higher. + TE_TEST If you are using a virtualenv, use the command `pip install --upgrade -e evennia` where `evennia` is the folder to where you cloned the Evennia library. If not in a virtualenv you can install django with for example `pip install --upgrade django` @@ -428,16 +431,15 @@ NOTE_KEYBOARDINTERRUPT = \ NOTE_TEST_DEFAULT = \ """ TESTING: Using Evennia's default settings file (evennia.settings_default). - (use 'evennia --settings settings.py test .' to run tests on the game dir) + (use 'evennia test --settings settings.py .' to run only your custom game tests) """ NOTE_TEST_CUSTOM = \ """ TESTING: Using specified settings file '{settings_dotpath}'. - (Obs: Evennia's full test suite may not pass if the settings are very - different from the default. Use 'test .' as arguments to run only tests - on the game dir.) + OBS: Evennia's full test suite may not pass if the settings are very + different from the default (use 'evennia test evennia' to run core tests) """ PROCESS_ERROR = \ @@ -519,7 +521,7 @@ def _print_info(portal_info_dict, server_info_dict): out = {} for key, value in dct.items(): if isinstance(value, list): - value = "\n{}".format(ind).join(value) + value = "\n{}".format(ind).join(str(val) for val in value) out[key] = value return out @@ -686,13 +688,14 @@ def send_instruction(operation, arguments, callback=None, errback=None): if AMP_CONNECTION: # already connected - send right away - _send() + return _send() else: # we must connect first, send once connected point = endpoints.TCP4ClientEndpoint(reactor, AMP_HOST, AMP_PORT) deferred = endpoints.connectProtocol(point, AMPLauncherProtocol()) deferred.addCallbacks(_on_connect, _on_connect_fail) REACTOR_RUN = True + return deferred def query_status(callback=None): @@ -2116,7 +2119,7 @@ def main(): else: kill(SERVER_PIDFILE, 'Server') elif option != "noop": - # pass-through to django manager + # pass-through to django manager, but set things up first check_db = False need_gamedir = True # some commands don't require the presence of a game directory to work @@ -2136,31 +2139,16 @@ def main(): init_game_directory(CURRENT_DIR, check_db=check_db, need_gamedir=need_gamedir) - # pass on to the manager - args = [option] - kwargs = {} - if unknown_args: - for arg in unknown_args: - if arg.startswith("--"): - print("arg:", arg) - if "=" in arg: - arg, value = [p.strip() for p in arg.split("=", 1)] - else: - value = True - kwargs[arg.lstrip("--")] = value - else: - args.append(arg) - - # makemessages needs a special syntax to not conflict with the -l option - if len(args) > 1 and args[0] == "makemessages": - args.insert(1, "-l") - - try: - django.core.management.call_command(*args, **kwargs) - except django.core.management.base.CommandError as exc: - args = ", ".join(args) - kwargs = ", ".join(["--%s" % kw for kw in kwargs]) - print(ERROR_INPUT.format(traceback=exc, args=args, kwargs=kwargs)) + if option == "migrate": + # we have to launch migrate within the program to make sure migrations + # run within the scope of the launcher (otherwise missing a db will cause errors) + django.core.management.call_command(*([option] + unknown_args)) + else: + # pass on to the core django manager - re-parse the entire input line + # but keep 'evennia' as the name instead of django-admin. This is + # an exit condition. + sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) + sys.exit(execute_from_command_line()) elif not args.tail_log: # no input; print evennia info (don't pring if we're tailing log) diff --git a/evennia/server/portal/amp.py b/evennia/server/portal/amp.py index ae0c123e87..831b79ff7b 100644 --- a/evennia/server/portal/amp.py +++ b/evennia/server/portal/amp.py @@ -282,6 +282,8 @@ class AMPMultiConnectionProtocol(amp.AMP): self.send_mode = True self.send_task = None self.multibatches = 0 + # later twisted amp has its own __init__ + super(AMPMultiConnectionProtocol, self).__init__(*args, **kwargs) def dataReceived(self, data): """ diff --git a/evennia/server/portal/portal.py b/evennia/server/portal/portal.py index 239e139746..3a632d23b8 100644 --- a/evennia/server/portal/portal.py +++ b/evennia/server/portal/portal.py @@ -105,7 +105,8 @@ class Portal(object): sys.path.append('.') # create a store of services - self.services = service.IServiceCollection(application) + self.services = service.MultiService() + self.services.setServiceParent(application) self.amp_protocol = None # set by amp factory self.sessions = PORTAL_SESSIONS self.sessions.portal = self diff --git a/evennia/server/profiling/tests.py b/evennia/server/profiling/tests.py index 29c9e36b9b..518d60b24c 100644 --- a/evennia/server/profiling/tests.py +++ b/evennia/server/profiling/tests.py @@ -103,6 +103,7 @@ class TestMemPlot(TestCase): @patch.object(memplot, "os") @patch.object(memplot, "open", new_callable=mock_open, create=True) @patch.object(memplot, "time") + @patch("evennia.utils.idmapper.models.SharedMemoryModel.flush_from_cache", new=Mock()) def test_memplot(self, mock_time, mocked_open, mocked_os, mocked_idmapper): if isinstance(memplot, Mock): return diff --git a/evennia/server/server.py b/evennia/server/server.py index cd56a73d59..3b3ff049f6 100644 --- a/evennia/server/server.py +++ b/evennia/server/server.py @@ -143,8 +143,6 @@ def _server_maintenance(): session.account.access(session.account, "noidletimeout", default=False): SESSIONS.disconnect(session, reason=reason) -maintenance_task = LoopingCall(_server_maintenance) -maintenance_task.start(60, now=True) # call every minute #------------------------------------------------------------ # Evennia Main Server object @@ -199,6 +197,7 @@ class Evennia(object): reactor.callLater(1, d.callback, None) reactor.sigInt = _wrap_sigint_handler + # Server startup methods def sqlite3_prep(self): @@ -303,6 +302,10 @@ class Evennia(object): """ from evennia.objects.models import ObjectDB + # start server time and maintenance task + self.maintenance_task = LoopingCall(_server_maintenance) + self.maintenance_task.start(60, now=True) # call every minute + # update eventual changed defaults self.update_defaults() @@ -322,6 +325,7 @@ class Evennia(object): # clear eventual lingering session storages ObjectDB.objects.clear_all_sessids() logger.log_msg("Evennia Server successfully started.") + # always call this regardless of start type self.at_server_start() diff --git a/evennia/server/tests/test_amp_connection.py b/evennia/server/tests/test_amp_connection.py new file mode 100644 index 0000000000..955a959b74 --- /dev/null +++ b/evennia/server/tests/test_amp_connection.py @@ -0,0 +1,126 @@ +""" +Test AMP client + +""" + +import pickle +from unittest import TestCase +from mock import MagicMock, patch +from twisted.trial.unittest import TestCase as TwistedTestCase +from evennia.server import amp_client +from evennia.server.portal import amp_server +from evennia.server.portal import amp +from evennia.server import server +from evennia.server.portal import portal +from evennia.server import serversession, session +from evennia.utils import create + +from twisted.internet.base import DelayedCall +DelayedCall.debug = True + +class _TestAMP(TwistedTestCase): + + def setUp(self): + super(_TestAMP, self).setUp() + with patch("evennia.server.initial_setup.get_god_account") as mockgod: + self.account = create.account("TestAMPAccount", "test@test.com", "testpassword") + mockgod.return_value = self.account + self.server = server.Evennia(MagicMock()) + self.server.sessions.data_in = MagicMock() + self.server.sessions.data_out = MagicMock() + self.amp_client_factory = amp_client.AMPClientFactory(self.server) + self.amp_client = self.amp_client_factory.buildProtocol("127.0.0.1") + self.session = MagicMock() # serversession.ServerSession() + self.session.sessid = 1 + self.server.sessions[1] = self.session + + self.portal = portal.Portal(MagicMock()) + self.portalsession = session.Session() + self.portalsession.sessid = 1 + self.portal.sessions[1] = self.portalsession + self.portal.sessions.data_in = MagicMock() + self.portal.sessions.data_out = MagicMock() + self.amp_server_factory = amp_server.AMPServerFactory(self.portal) + self.amp_server = self.amp_server_factory.buildProtocol("127.0.0.1") + + def tearDown(self): + self.account.delete() + super(_TestAMP, self).tearDown() + + def _connect_client(self, mocktransport): + "Setup client to send data for testing" + mocktransport.write = MagicMock() + self.amp_client.makeConnection(mocktransport) + mocktransport.write.reset_mock() + + def _connect_server(self, mocktransport): + "Setup server to send data for testing" + mocktransport.write = MagicMock() + self.amp_server.makeConnection(mocktransport) + mocktransport.write.reset_mock() + + def _catch_wire_read(self, mocktransport): + "Parse what was supposed to be sent over the wire" + arg_list = mocktransport.write.call_args_list + + all_sent = [] + for i, cll in enumerate(arg_list): + args, kwargs = cll + raw_inp = args[0] + all_sent.append(raw_inp) + + return all_sent + + +@patch("evennia.server.server.LoopingCall", MagicMock()) +@patch("evennia.server.portal.amp.amp.BinaryBoxProtocol.transport") +class TestAMPClientSend(_TestAMP): + """Test amp client sending data""" + + def test_msgserver2portal(self, mocktransport): + self._connect_client(mocktransport) + self.amp_client.send_MsgServer2Portal(self.session, text={"foo": "bar"}) + wire_data = self._catch_wire_read(mocktransport)[0] + + self._connect_server(mocktransport) + self.amp_server.dataReceived(wire_data) + self.portal.sessions.data_out.assert_called_with(self.portalsession, text={"foo": "bar"}) + + def test_adminserver2portal(self, mocktransport): + self._connect_client(mocktransport) + + self.amp_client.send_AdminServer2Portal(self.session, + operation=amp.PSYNC, + info_dict={}, spid=None) + wire_data = self._catch_wire_read(mocktransport)[0] + + self._connect_server(mocktransport) + self.amp_server.data_in = MagicMock() + self.amp_server.dataReceived(wire_data) + self.amp_server.data_in.assert_called() + + +@patch("evennia.server.portal.amp.amp.BinaryBoxProtocol.transport") +class TestAMPClientRecv(_TestAMP): + """Test amp client sending data""" + + def test_msgportal2server(self, mocktransport): + self._connect_server(mocktransport) + self.amp_server.send_MsgPortal2Server(self.session, text={"foo": "bar"}) + wire_data = self._catch_wire_read(mocktransport)[0] + + self._connect_client(mocktransport) + self.amp_client.dataReceived(wire_data) + self.server.sessions.data_in.assert_called_with(self.session, text={"foo": "bar"}) + + def test_adminportal2server(self, mocktransport): + self._connect_server(mocktransport) + + self.amp_server.send_AdminPortal2Server(self.session, + operation=amp.PDISCONNALL) + wire_data = self._catch_wire_read(mocktransport)[0] + + self._connect_client(mocktransport) + self.server.sessions.portal_disconnect_all = MagicMock() + self.amp_client.dataReceived(wire_data) + self.server.sessions.portal_disconnect_all.assert_called() diff --git a/evennia/server/tests/test_launcher.py b/evennia/server/tests/test_launcher.py new file mode 100644 index 0000000000..231aab8aa6 --- /dev/null +++ b/evennia/server/tests/test_launcher.py @@ -0,0 +1,197 @@ +""" +Test the evennia launcher. + +""" + +import os +import pickle +from anything import Something +from mock import patch, MagicMock, create_autospec +from twisted.internet import reactor +from twisted.trial.unittest import TestCase as TwistedTestCase +from evennia.server import evennia_launcher +from evennia.server.portal import amp + +from twisted.internet.base import DelayedCall +DelayedCall.debug = True + +@patch("evennia.server.evennia_launcher.Popen", new=MagicMock()) +class TestLauncher(TwistedTestCase): + + def test_is_windows(self): + self.assertEqual(evennia_launcher._is_windows(), os.name == 'nt') + + def test_file_compact(self): + self.assertEqual(evennia_launcher._file_names_compact( + "foo/bar/test1", "foo/bar/test2"), + "foo/bar/test1 and test2") + + self.assertEqual(evennia_launcher._file_names_compact( + "foo/test1", "foo/bar/test2"), + "foo/test1 and foo/bar/test2") + + @patch("evennia.server.evennia_launcher.print") + def test_print_info(self, mockprint): + portal_dict = { + "servername": "testserver", + "version": "1", + "telnet": 1234, + "telnet_ssl": [1234, 2345], + "ssh": 1234, + "webserver_proxy": 1234, + "webclient": 1234, + "webserver_internal": 1234, + "amp": 1234 + } + server_dict = { + "servername": "testserver", + "version": "1", + "webserver": [1234, 1234], + "amp": 1234, + "irc_rss": "irc.test", + "info": "testing mode", + "errors": "" + } + + evennia_launcher._print_info(portal_dict, server_dict) + mockprint.assert_called() + + def test_parse_status(self): + response = {"status": pickle.dumps(("teststring",))} + result = evennia_launcher._parse_status(response) + self.assertEqual(result, ("teststring",)) + + @patch("evennia.server.evennia_launcher.os.name", new="posix") + def test_get_twisted_cmdline(self): + pcmd, scmd = evennia_launcher._get_twistd_cmdline(False, False) + self.assertTrue("portal.py" in pcmd[1]) + self.assertTrue("--pidfile" in pcmd[2]) + self.assertTrue("server.py" in scmd[1]) + self.assertTrue("--pidfile" in scmd[2]) + + pcmd, scmd = evennia_launcher._get_twistd_cmdline(True, True) + self.assertTrue("portal.py" in pcmd[1]) + self.assertTrue("--pidfile" in pcmd[2]) + self.assertTrue("--profiler=cprofile" in pcmd[4], "actual: {}".format(pcmd)) + self.assertTrue("--profile=" in pcmd[5]) + self.assertTrue("server.py" in scmd[1]) + self.assertTrue("--pidfile" in scmd[2]) + self.assertTrue("--pidfile" in scmd[2]) + self.assertTrue("--profiler=cprofile" in scmd[4], "actual: {}".format(scmd)) + self.assertTrue("--profile=" in scmd[5]) + + @patch("evennia.server.evennia_launcher.os.name", new="nt") + def test_get_twisted_cmdline_nt(self): + pcmd, scmd = evennia_launcher._get_twistd_cmdline(False, False) + self.assertTrue(len(pcmd) == 2, "actual: {}".format(pcmd)) + self.assertTrue(len(scmd) == 2, "actual: {}".format(scmd)) + + @patch("evennia.server.evennia_launcher.reactor.stop") + def test_reactor_stop(self, mockstop): + evennia_launcher._reactor_stop() + mockstop.assert_called() + + def _catch_wire_read(self, mocktransport): + "Parse what was supposed to be sent over the wire" + arg_list = mocktransport.write.call_args_list + + all_sent = [] + for i, cll in enumerate(arg_list): + args, kwargs = cll + raw_inp = args[0] + all_sent.append(raw_inp) + + return all_sent + + # @patch("evennia.server.portal.amp.amp.BinaryBoxProtocol.transport") + # def test_send_instruction_pstatus(self, mocktransport): + + # deferred = evennia_launcher.send_instruction( + # evennia_launcher.PSTATUS, + # (), + # callback=MagicMock(), + # errback=MagicMock()) + + # on_wire = self._catch_wire_read(mocktransport) + # self.assertEqual(on_wire, "") + + # return deferred + + def _msend_status_ok(operation, arguments, callback=None, errback=None): + callback({"status": pickle.dumps((True, True, 2, 24, "info1", "info2"))}) + + def _msend_status_err(operation, arguments, callback=None, errback=None): + errback({"status": pickle.dumps((False, False, 3, 25, "info3", "info4"))}) + + @patch("evennia.server.evennia_launcher.send_instruction", _msend_status_ok) + @patch("evennia.server.evennia_launcher.NO_REACTOR_STOP", True) + @patch("evennia.server.evennia_launcher.get_pid", MagicMock(return_value=100)) + @patch("evennia.server.evennia_launcher.print") + def test_query_status_run(self, mprint): + evennia_launcher.query_status() + mprint.assert_called_with('Portal: RUNNING (pid 100)\nServer: RUNNING (pid 100)') + + @patch("evennia.server.evennia_launcher.send_instruction", _msend_status_err) + @patch("evennia.server.evennia_launcher.NO_REACTOR_STOP", True) + @patch("evennia.server.evennia_launcher.print") + def test_query_status_not_run(self, mprint): + evennia_launcher.query_status() + mprint.assert_called_with('Portal: NOT RUNNING\nServer: NOT RUNNING') + + @patch("evennia.server.evennia_launcher.send_instruction", _msend_status_ok) + @patch("evennia.server.evennia_launcher.NO_REACTOR_STOP", True) + def test_query_status_callback(self): + mprint = MagicMock() + + def testcall(response): + resp = pickle.loads(response['status']) + mprint(resp) + + evennia_launcher.query_status(callback=testcall) + mprint.assert_called_with((True, True, 2, 24, "info1", "info2")) + + @patch("evennia.server.evennia_launcher.AMP_CONNECTION") + @patch("evennia.server.evennia_launcher.print") + def test_wait_for_status_reply(self, mprint, aconn): + aconn.wait_for_status = MagicMock() + + def test(): + pass + + evennia_launcher.wait_for_status_reply(test) + aconn.wait_for_status.assert_called_with(test) + + @patch("evennia.server.evennia_launcher.AMP_CONNECTION", None) + @patch("evennia.server.evennia_launcher.print") + def test_wait_for_status_reply_fail(self, mprint): + evennia_launcher.wait_for_status_reply(None) + mprint.assert_called_with("No Evennia connection established.") + + @patch("evennia.server.evennia_launcher.send_instruction", _msend_status_ok) + @patch("evennia.server.evennia_launcher.reactor.callLater") + def test_wait_for_status(self, mcalllater): + mcall = MagicMock() + merr = MagicMock() + evennia_launcher.wait_for_status( + portal_running=True, + server_running=True, + callback=mcall, + errback=merr) + + mcall.assert_called_with(True, True) + merr.assert_not_called() + + @patch("evennia.server.evennia_launcher.send_instruction", _msend_status_err) + @patch("evennia.server.evennia_launcher.reactor.callLater") + def test_wait_for_status_fail(self, mcalllater): + mcall = MagicMock() + merr = MagicMock() + evennia_launcher.wait_for_status( + portal_running=True, + server_running=True, + callback=mcall, + errback=merr) + + mcall.assert_not_called() + merr.assert_not_called() + mcalllater.assert_called() diff --git a/evennia/server/tests/test_server.py b/evennia/server/tests/test_server.py new file mode 100644 index 0000000000..bc971af7a4 --- /dev/null +++ b/evennia/server/tests/test_server.py @@ -0,0 +1,242 @@ +""" +Test the main server component + +""" + +from unittest import TestCase +from mock import MagicMock, patch, DEFAULT, call +from django.test import override_settings +from evennia.utils.test_resources import unload_module + + +@patch("evennia.server.server.LoopingCall", new=MagicMock()) +class TestServer(TestCase): + """ + Test server module. + + """ + + def setUp(self): + from evennia.server import server + self.server = server + + def test__server_maintenance_reset(self): + with patch.multiple("evennia.server.server", + LoopingCall=DEFAULT, + Evennia=DEFAULT, + _FLUSH_CACHE=DEFAULT, + connection=DEFAULT, + _IDMAPPER_CACHE_MAXSIZE=1000, + _MAINTENANCE_COUNT=0, + ServerConfig=DEFAULT) as mocks: + mocks['connection'].close = MagicMock() + mocks['ServerConfig'].objects.conf = MagicMock(return_value=456) + + # flush cache + self.server._server_maintenance() + mocks['ServerConfig'].objects.conf.assert_called_with('runtime', 456) + + def test__server_maintenance_flush(self): + with patch.multiple("evennia.server.server", + LoopingCall=DEFAULT, + Evennia=DEFAULT, + _FLUSH_CACHE=DEFAULT, + connection=DEFAULT, + _IDMAPPER_CACHE_MAXSIZE=1000, + _MAINTENANCE_COUNT=600 - 1, + ServerConfig=DEFAULT) as mocks: + mocks['connection'].close = MagicMock() + mocks['ServerConfig'].objects.conf = MagicMock(return_value=100) + + # flush cache + self.server._server_maintenance() + mocks['_FLUSH_CACHE'].assert_called_with(1000) + + def test__server_maintenance_validate_scripts(self): + with patch.multiple("evennia.server.server", + LoopingCall=DEFAULT, + Evennia=DEFAULT, + _FLUSH_CACHE=DEFAULT, + connection=DEFAULT, + _IDMAPPER_CACHE_MAXSIZE=1000, + _MAINTENANCE_COUNT=3600 - 1, + ServerConfig=DEFAULT) as mocks: + mocks['connection'].close = MagicMock() + mocks['ServerConfig'].objects.conf = MagicMock(return_value=100) + with patch("evennia.server.server.evennia.ScriptDB.objects.validate") as mock: + self.server._server_maintenance() + mocks['_FLUSH_CACHE'].assert_called_with(1000) + mock.assert_called() + + def test__server_maintenance_channel_handler_update(self): + with patch.multiple("evennia.server.server", + LoopingCall=DEFAULT, + Evennia=DEFAULT, + _FLUSH_CACHE=DEFAULT, + connection=DEFAULT, + _IDMAPPER_CACHE_MAXSIZE=1000, + _MAINTENANCE_COUNT=3700 - 1, + ServerConfig=DEFAULT) as mocks: + mocks['connection'].close = MagicMock() + mocks['ServerConfig'].objects.conf = MagicMock(return_value=100) + with patch("evennia.server.server.evennia.CHANNEL_HANDLER.update") as mock: + self.server._server_maintenance() + mock.assert_called() + + def test__server_maintenance_close_connection(self): + with patch.multiple("evennia.server.server", + LoopingCall=DEFAULT, + Evennia=DEFAULT, + _FLUSH_CACHE=DEFAULT, + connection=DEFAULT, + _IDMAPPER_CACHE_MAXSIZE=1000, + _MAINTENANCE_COUNT=(3600 * 7) - 1, + ServerConfig=DEFAULT) as mocks: + mocks['connection'].close = MagicMock() + mocks['ServerConfig'].objects.conf = MagicMock(return_value=100) + self.server._server_maintenance() + mocks['connection'].close.assert_called() + + def test__server_maintenance_idle_time(self): + with patch.multiple("evennia.server.server", + LoopingCall=DEFAULT, + Evennia=DEFAULT, + _FLUSH_CACHE=DEFAULT, + connection=DEFAULT, + _IDMAPPER_CACHE_MAXSIZE=1000, + _MAINTENANCE_COUNT=(3600 * 7) - 1, + SESSIONS=DEFAULT, + _IDLE_TIMEOUT=10, + time=DEFAULT, + ServerConfig=DEFAULT) as mocks: + sess1 = MagicMock() + sess2 = MagicMock() + sess3 = MagicMock() + sess4 = MagicMock() + sess1.cmd_last = 100 # should time out + sess2.cmd_last = 999 # should not time out + sess3.cmd_last = 100 # should not time (due to account) + sess4.cmd_last = 100 # should time out (due to access) + sess1.account = None + sess2.account = None + sess3.account = MagicMock() + sess3.account = MagicMock() + sess4.account.access = MagicMock(return_value=False) + + mocks['time'].time = MagicMock(return_value=1000) + + mocks['ServerConfig'].objects.conf = MagicMock(return_value=100) + mocks["SESSIONS"].values = MagicMock(return_value=[sess1, sess2, sess3, sess4]) + mocks["SESSIONS"].disconnect = MagicMock() + + self.server._server_maintenance() + reason = "idle timeout exceeded" + calls = [call(sess1, reason=reason), call(sess4, reason=reason)] + mocks["SESSIONS"].disconnect.assert_has_calls(calls, any_order=True) + + def test_evennia_start(self): + with patch.multiple("evennia.server.server", + time=DEFAULT, + service=DEFAULT) as mocks: + + mocks['time'].time = MagicMock(return_value=1000) + evennia = self.server.Evennia(MagicMock()) + self.assertEqual(evennia.start_time, 1000) + + @patch("evennia.objects.models.ObjectDB") + @patch("evennia.server.server.AccountDB") + @patch("evennia.server.server.ScriptDB") + @patch("evennia.comms.models.ChannelDB") + def test_update_defaults(self, mockchan, mockscript, mockacct, mockobj): + with patch.multiple("evennia.server.server", + ServerConfig=DEFAULT) as mocks: + + mockchan.objects.filter = MagicMock() + mockscript.objects.filter = MagicMock() + mockacct.objects.filter = MagicMock() + mockobj.objects.filter = MagicMock() + + # fake mismatches + settings_names = ("CMDSET_CHARACTER", "CMDSET_ACCOUNT", + "BASE_ACCOUNT_TYPECLASS", "BASE_OBJECT_TYPECLASS", + "BASE_CHARACTER_TYPECLASS", "BASE_ROOM_TYPECLASS", + "BASE_EXIT_TYPECLASS", "BASE_SCRIPT_TYPECLASS", + "BASE_CHANNEL_TYPECLASS") + fakes = {name: "Dummy.path" for name in settings_names} + + def _mock_conf(key, *args): + return fakes[key] + + mocks['ServerConfig'].objects.conf = _mock_conf + + evennia = self.server.Evennia(MagicMock()) + evennia.update_defaults() + + mockchan.objects.filter.assert_called() + mockscript.objects.filter.assert_called() + mockacct.objects.filter.assert_called() + mockobj.objects.filter.assert_called() + + def test_initial_setup(self): + from evennia.utils.create import create_account + + acct = create_account("TestSuperuser", "test@test.com", "testpassword", + is_superuser=True) + + with patch.multiple("evennia.server.initial_setup", + reset_server=DEFAULT, + AccountDB=DEFAULT) as mocks: + mocks['AccountDB'].objects.get = MagicMock(return_value=acct) + evennia = self.server.Evennia(MagicMock()) + evennia.run_initial_setup() + acct.delete() + + def test_initial_setup_retry(self): + from evennia.utils.create import create_account + + acct = create_account("TestSuperuser2", "test@test.com", "testpassword", + is_superuser=True) + + with patch.multiple("evennia.server.initial_setup", + ServerConfig=DEFAULT, + reset_server=DEFAULT, + AccountDB=DEFAULT) as mocks: + mocks['AccountDB'].objects.get = MagicMock(return_value=acct) + # a last_initial_setup_step > 0 + mocks['ServerConfig'].objects.conf = MagicMock(return_value=4) + evennia = self.server.Evennia(MagicMock()) + evennia.run_initial_setup() + acct.delete() + + @override_settings(DEFAULT_HOME="#1") + def test_run_init_hooks(self): + from evennia.utils import create + obj1 = create.object(key="HookTestObj1") + obj2 = create.object(key="HookTestObj2") + acct1 = create.account("HookAcct1", "hooktest1@test.com", "testpasswd") + acct2 = create.account("HookAcct2", "hooktest2@test.com", "testpasswd") + + with patch("evennia.objects.models.ObjectDB") as mockobj: + with patch("evennia.server.server.AccountDB") as mockacct: + + mockacct.get_all_cached_instances = MagicMock(return_value=[acct1, acct2]) + mockobj.get_all_cached_instances = MagicMock(return_value=[obj1, obj2]) + mockobj.objects.clear_all_sessids = MagicMock() + + evennia = self.server.Evennia(MagicMock()) + evennia.run_init_hooks('reload') + evennia.run_init_hooks('reset') + evennia.run_init_hooks('shutdown') + + mockacct.get_all_cached_instances.assert_called() + mockobj.get_all_cached_instances.assert_called() + mockobj.objects.clear_all_sessids.assert_called_with() + obj1.delete() + obj2.delete() + acct1.delete() + acct2.delete() + + @patch('evennia.server.server.INFO_DICT', {"test": "foo"}) + def test_get_info_dict(self): + evennia = self.server.Evennia(MagicMock()) + self.assertEqual(evennia.get_info_dict(), {"test": "foo"}) diff --git a/evennia/typeclasses/attributes.py b/evennia/typeclasses/attributes.py index 5f18d7d14e..e64b08c450 100644 --- a/evennia/typeclasses/attributes.py +++ b/evennia/typeclasses/attributes.py @@ -383,7 +383,9 @@ class AttributeHandler(object): """ ret = [] + category = category.strip().lower() if category is not None else None for keystr in make_iter(key): + keystr = key.strip().lower() ret.extend(bool(attr) for attr in self._getcache(keystr, category)) return ret[0] if len(ret) == 1 else ret @@ -605,7 +607,8 @@ class AttributeHandler(object): Remove attribute or a list of attributes from object. Args: - key (str): An Attribute key to remove. + key (str or list): An Attribute key to remove or a list of keys. If + multiple keys, they must all be of the same `category`. raise_exception (bool, optional): If set, not finding the Attribute to delete will raise an exception instead of just quietly failing. @@ -623,7 +626,11 @@ class AttributeHandler(object): was found matching `key`. """ + category = category.strip().lower() if category is not None else None + for keystr in make_iter(key): + keystr = keystr.lower() + attr_objs = self._getcache(keystr, category) for attr_obj in attr_objs: if not ( @@ -634,10 +641,11 @@ class AttributeHandler(object): try: attr_obj.delete() except AssertionError: + print("Assertionerror for attr.delete()") # this happens if the attr was already deleted pass finally: - self._delcache(key, category) + self._delcache(keystr, category) if not attr_objs and raise_exception: raise AttributeError @@ -655,6 +663,8 @@ class AttributeHandler(object): type `attredit` on the Attribute in question. """ + category = category.strip().lower() if category is not None else None + if not self._cache_complete: self._fullcache() if accessing_obj: diff --git a/evennia/utils/create.py b/evennia/utils/create.py index 8ae5d00298..d3a22be866 100644 --- a/evennia/utils/create.py +++ b/evennia/utils/create.py @@ -402,6 +402,7 @@ def create_account(key, email, password, typeclass=None, is_superuser=False, locks=None, permissions=None, + tags=None, attributes=None, report_to=None): """ This creates a new account. @@ -414,15 +415,20 @@ def create_account(key, email, password, password (str): Password in cleartext. Kwargs: + typeclass (str): The typeclass to use for the account. is_superuser (bool): Wether or not this account is to be a superuser locks (str): Lockstring. permission (list): List of permission strings. + tags (list): List of Tags on form `(key, category[, data])` + attributes (list): List of Attributes on form + `(key, value [, category, [,lockstring [, default_pass]]])` report_to (Object): An object with a msg() method to report errors to. If not given, errors will be logged. Raises: ValueError: If `key` already exists in database. + Notes: Usually only the server admin should need to be superuser, all other access levels can be handled with more fine-grained @@ -437,6 +443,8 @@ def create_account(key, email, password, typeclass = typeclass if typeclass else settings.BASE_ACCOUNT_TYPECLASS locks = make_iter(locks) if locks is not None else None permissions = make_iter(permissions) if permissions is not None else None + tags = make_iter(tags) if tags is not None else None + attributes = make_iter(attributes) if attributes is not None else None if isinstance(typeclass, str): # a path is given. Load the actual typeclass. @@ -462,7 +470,8 @@ def create_account(key, email, password, is_staff=is_superuser, is_superuser=is_superuser, last_login=now, date_joined=now) new_account.set_password(password) - new_account._createdict = dict(locks=locks, permissions=permissions, report_to=report_to) + new_account._createdict = dict(locks=locks, permissions=permissions, report_to=report_to, + tags=tags, attributes=attributes) # saving will trigger the signal that calls the # at_first_save hook on the typeclass, where the _createdict # can be used. diff --git a/evennia/utils/eveditor.py b/evennia/utils/eveditor.py index 0c36da7a5e..7a2fbc9b70 100644 --- a/evennia/utils/eveditor.py +++ b/evennia/utils/eveditor.py @@ -510,7 +510,7 @@ class CmdEditorGroup(CmdEditorBase): else: buf = linebuffer[:lstart] + editor._copy_buffer + linebuffer[lstart:] editor.update_buffer(buf) - caller.msg("Copied buffer %s to %s." % (editor._copy_buffer, self.lstr)) + caller.msg("Pasted buffer %s to %s." % (editor._copy_buffer, self.lstr)) elif cmd == ":i": # :i - insert new line new_lines = self.args.split('\n') diff --git a/evennia/utils/test_resources.py b/evennia/utils/test_resources.py index 2fdebaa85e..e73d76daf8 100644 --- a/evennia/utils/test_resources.py +++ b/evennia/utils/test_resources.py @@ -1,3 +1,8 @@ +""" +Various helper resources for writing unittests. + +""" +import sys from django.conf import settings from django.test import TestCase from mock import Mock @@ -10,8 +15,38 @@ from evennia.utils import create from evennia.utils.idmapper.models import flush_cache -SESSIONS.data_out = Mock() -SESSIONS.disconnect = Mock() +def unload_module(module): + """ + Reset import so one can mock global constants. + + Args: + module (module, object or str): The module will + be removed so it will have to be imported again. If given + an object, the module in which that object sits will be unloaded. A string + should directly give the module pathname to unload. + + Example: + # (in a test method) + unload_module(foo) + with mock.patch("foo.GLOBALTHING", "mockval"): + import foo + ... # test code using foo.GLOBALTHING, now set to 'mockval' + + + This allows for mocking constants global to the module, since + otherwise those would not be mocked (since a module is only + loaded once). + + """ + if isinstance(module, basestring): + modulename = module + elif hasattr(module, "__module__"): + modulename = module.__module__ + else: + modulename = module.__name__ + + if modulename in sys.modules: + del sys.modules[modulename] class EvenniaTest(TestCase): @@ -29,6 +64,11 @@ class EvenniaTest(TestCase): """ Sets up testing environment """ + self.backups = (SESSIONS.data_out, SESSIONS.disconnect, + settings.DEFAULT_HOME, settings.PROTOTYPE_MODULES) + SESSIONS.data_out = Mock() + SESSIONS.disconnect = Mock() + self.account = create.create_account("TestAccount", email="test@test.com", password="testpassword", typeclass=self.account_typeclass) self.account2 = create.create_account("TestAccount2", email="test@test.com", password="testpassword", typeclass=self.account_typeclass) self.room1 = create.create_object(self.room_typeclass, key="Room", nohome=True) @@ -62,6 +102,11 @@ class EvenniaTest(TestCase): def tearDown(self): flush_cache() + SESSIONS.data_out = self.backups[0] + SESSIONS.disconnect = self.backups[1] + settings.DEFAULT_HOME = self.backups[2] + settings.PROTOTYPE_MODULES = self.backups[3] + del SESSIONS[self.session.sessid] self.account.delete() self.account2.delete() diff --git a/evennia/utils/tests/test_eveditor.py b/evennia/utils/tests/test_eveditor.py new file mode 100644 index 0000000000..5fbce57c00 --- /dev/null +++ b/evennia/utils/tests/test_eveditor.py @@ -0,0 +1,93 @@ +""" +Test eveditor + +""" + +from mock import MagicMock +from django.test import TestCase +from evennia.utils import eveditor +from evennia.commands.default.tests import CommandTest + + +class TestEvEditor(CommandTest): + + def test_eveditor_view_cmd(self): + eveditor.EvEditor(self.char1) + self.call(eveditor.CmdEditorGroup(), "", cmdstring=":h", + msg=" - any non-command is appended to the end of the buffer.") + # empty buffer + self.call(eveditor.CmdEditorGroup(), "", cmdstring=":", + msg="Line Editor []\n01\n[l:01 w:000 c:0000](:h for help)") + # input a string + self.call(eveditor.CmdLineInput(), "First test line", raw_string="First test line", + msg="01First test line") + self.call(eveditor.CmdLineInput(), "Second test line", raw_string="Second test line", + msg="02Second test line") + self.assertEqual(self.char1.ndb._eveditor.get_buffer(), 'First test line\nSecond test line') + + self.call(eveditor.CmdEditorGroup(), "", cmdstring=":", # view buffer + msg="Line Editor []\n01First test line\n" + "02Second test line\n[l:02 w:006 c:0032](:h for help)") + self.call(eveditor.CmdEditorGroup(), "", cmdstring="::", # view buffer, no linenums + msg="Line Editor []\nFirst test line\n" + "Second test line\n[l:02 w:006 c:0032](:h for help)") + self.call(eveditor.CmdEditorGroup(), "", cmdstring=":::", # add single : alone on row + msg="Single ':' added to buffer.") + self.call(eveditor.CmdEditorGroup(), "", cmdstring=":", + msg="Line Editor []\n01First test line\n" + "02Second test line\n03:\n[l:03 w:007 c:0034](:h for help)") + + self.call(eveditor.CmdEditorGroup(), "", cmdstring=":dd", # delete line + msg="Deleted line 3.") + self.assertEqual(self.char1.ndb._eveditor.get_buffer(), + 'First test line\nSecond test line') + self.call(eveditor.CmdEditorGroup(), "", cmdstring=":u", # undo + msg="Undid one step.") + self.assertEqual(self.char1.ndb._eveditor.get_buffer(), + 'First test line\nSecond test line\n:') + self.call(eveditor.CmdEditorGroup(), "", cmdstring=":uu", # redo + msg="Redid one step.") + self.assertEqual(self.char1.ndb._eveditor.get_buffer(), + 'First test line\nSecond test line') + self.call(eveditor.CmdEditorGroup(), "", cmdstring=":u", # undo + msg="Undid one step.") + self.assertEqual(self.char1.ndb._eveditor.get_buffer(), + 'First test line\nSecond test line\n:') + self.call(eveditor.CmdEditorGroup(), "", cmdstring=":", + msg="Line Editor []\n01First test line\n" + "02Second test line\n03:\n[l:03 w:007 c:0034](:h for help)") + + self.call(eveditor.CmdEditorGroup(), "Second", cmdstring=":dw", # delete by word + msg="Removed Second for lines 1-4.") + self.call(eveditor.CmdEditorGroup(), "", cmdstring=":u", # undo + msg="Undid one step.") + self.call(eveditor.CmdEditorGroup(), "2 Second", cmdstring=":dw", # delete by word/line + msg="Removed Second for line 2.") + self.assertEqual(self.char1.ndb._eveditor.get_buffer(), + 'First test line\n test line\n:') + + + self.call(eveditor.CmdEditorGroup(), "2", cmdstring=":p", # paste + msg="Copy buffer is empty.") + self.call(eveditor.CmdEditorGroup(), "2", cmdstring=":y", # yank + msg="Line 2, [' test line'] yanked.") + self.call(eveditor.CmdEditorGroup(), "2", cmdstring=":p", # paste + msg="Pasted buffer [' test line'] to line 2.") + self.assertEqual(self.char1.ndb._eveditor.get_buffer(), + 'First test line\n test line\n test line\n:') + + self.call(eveditor.CmdEditorGroup(), "3", cmdstring=":x", # cut + msg="Line 3, [' test line'] cut.") + + self.call(eveditor.CmdEditorGroup(), "2 New Second line", cmdstring=":i", # insert + msg="Inserted 1 new line(s) at line 2.") + self.call(eveditor.CmdEditorGroup(), "2 New Replaced Second line", # replace + cmdstring=":r", msg="Replaced 1 line(s) at line 2.") + self.call(eveditor.CmdEditorGroup(), "2 Inserted-", # insert beginning line + cmdstring=":I", msg="Inserted text at beginning of line 2.") + self.call(eveditor.CmdEditorGroup(), "2 -End", # append end line + cmdstring=":A", msg="Appended text to end of line 2.") + + self.assertEqual( + self.char1.ndb._eveditor.get_buffer(), + 'First test line\nInserted-New Replaced Second line-End\n test line\n:') diff --git a/evennia/utils/text2html.py b/evennia/utils/text2html.py index 64a25f5742..88f2101d3c 100644 --- a/evennia/utils/text2html.py +++ b/evennia/utils/text2html.py @@ -215,6 +215,7 @@ class TextToHTMLparser(object): text (str): Processed text. """ + return text return text.replace(r'\n', r'
') def convert_urls(self, text): diff --git a/evennia/web/website/templates/website/404.html b/evennia/web/website/templates/website/404.html index f98e6522a1..562e9b8bb7 100644 --- a/evennia/web/website/templates/website/404.html +++ b/evennia/web/website/templates/website/404.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "website/base.html" %} {% block titleblock %}404 - Not Found{% endblock %} @@ -10,7 +10,7 @@
-
+

Error 404

Page not found


diff --git a/evennia/web/website/templates/website/500.html b/evennia/web/website/templates/website/500.html index f40ed64725..063cd3e3ab 100644 --- a/evennia/web/website/templates/website/500.html +++ b/evennia/web/website/templates/website/500.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "website/base.html" %} {% block titleblock %}500 - Internal Server Error{% endblock %} @@ -10,7 +10,7 @@
-
+

Error 500

Internal Server Error


diff --git a/evennia/web/website/templates/website/evennia_admin.html b/evennia/web/website/templates/website/evennia_admin.html index 80e22e2f2a..e01aee2380 100644 --- a/evennia/web/website/templates/website/evennia_admin.html +++ b/evennia/web/website/templates/website/evennia_admin.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "website/base.html" %} {% block titleblock %}Admin{% endblock %} @@ -6,7 +6,7 @@
-
+

Admin

Welcome to the Evennia Admin Page. Here, you can edit many facets of accounts, characters, and other parts of the game. diff --git a/evennia/web/website/templates/website/flatpages/default.html b/evennia/web/website/templates/website/flatpages/default.html index 5184c22deb..cc9b03a670 100644 --- a/evennia/web/website/templates/website/flatpages/default.html +++ b/evennia/web/website/templates/website/flatpages/default.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "website/base.html" %} {% block header_ext %} {% endblock %} @@ -7,7 +7,7 @@

-
+

{{flatpage.title}}

{{flatpage.content}}
diff --git a/evennia/web/website/templates/website/index.html b/evennia/web/website/templates/website/index.html index 06ece50f3c..3c7a99df43 100644 --- a/evennia/web/website/templates/website/index.html +++ b/evennia/web/website/templates/website/index.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "website/base.html" %} {% block titleblock %}Home{% endblock %} diff --git a/evennia/web/website/templates/website/registration/logged_out.html b/evennia/web/website/templates/website/registration/logged_out.html index 0ee9cdcfe4..2eadfd61ec 100644 --- a/evennia/web/website/templates/website/registration/logged_out.html +++ b/evennia/web/website/templates/website/registration/logged_out.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "website/base.html" %} {% block header_ext %} diff --git a/evennia/web/website/templates/website/registration/login.html b/evennia/web/website/templates/website/registration/login.html index cf798e8f76..dab2a82be2 100644 --- a/evennia/web/website/templates/website/registration/login.html +++ b/evennia/web/website/templates/website/registration/login.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "website/base.html" %} {% block titleblock %} Login diff --git a/evennia/web/website/templates/website/tbi.html b/evennia/web/website/templates/website/tbi.html index b303a270c8..ea3669ffce 100644 --- a/evennia/web/website/templates/website/tbi.html +++ b/evennia/web/website/templates/website/tbi.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "website/base.html" %} {% block titleblock %}To Be Implemented{% endblock %} @@ -12,7 +12,7 @@

To Be Implemented...

-
+
This feature has yet to be implemented, but rest assured that it will be!