From 502ebff1a2384cc502363cde2a67cd041e869763 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 21 Nov 2010 19:02:24 +0000 Subject: [PATCH] Implemented a unit testing framework for Evennia. Unfortunately it seems it is only usable in latest Django SVN, due to a Django bug; Run "manage.py test-evennia" - if you get errors about SUPPORTS_TRANSACTIONS, you are affected by the bug. Since this is only likely to affect evennia devs at this point I added a few base tests in src/objects/tests.py as a template for those willing to help add unit tests. --- game/gamesrc/commands/default/general.py | 2 +- game/manage.py | 55 +++++++- src/commands/cmdhandler.py | 18 ++- src/objects/tests.py | 153 +++++++++++++++++++++++ src/permissions/permissions.py | 5 +- src/utils/create.py | 2 +- src/utils/reloads.py | 7 +- 7 files changed, 227 insertions(+), 15 deletions(-) create mode 100644 src/objects/tests.py diff --git a/game/gamesrc/commands/default/general.py b/game/gamesrc/commands/default/general.py index 2ea474d758..c562caa6af 100644 --- a/game/gamesrc/commands/default/general.py +++ b/game/gamesrc/commands/default/general.py @@ -92,7 +92,7 @@ class CmdPassword(MuxCommand): oldpass = self.lhslist[0] # this is already stripped by parse() newpass = self.rhslist[0] # '' try: - uaccount = caller.user + uaccount = caller.player.user except AttributeError: caller.msg("This is only applicable for players.") return diff --git a/game/manage.py b/game/manage.py index 052f4be455..eeb2451151 100755 --- a/game/manage.py +++ b/game/manage.py @@ -11,11 +11,18 @@ import os # Tack on the root evennia directory to the python path. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +#------------------------------------------------------------ +# Get Evennia version +#------------------------------------------------------------ try: VERSION = open("%s%s%s" % (os.pardir, os.sep, 'VERSION')).readline().strip() except IOError: VERSION = "Unknown version" +#------------------------------------------------------------ +# Check so session file exists in the current dir- if not, create it. +#------------------------------------------------------------ + _CREATED_SETTINGS = False if not os.path.exists('settings.py'): # If settings.py doesn't already exist, create it and populate it with some @@ -88,7 +95,8 @@ from src.settings_default import * ################################################### # Evennia components (django apps) -###################################################""" +################################################### +""" settings_file.write(string) settings_file.close() @@ -97,6 +105,9 @@ from src.settings_default import * Welcome to Evennia (version %s)! We created a fresh settings.py file for you.""" % VERSION +#------------------------------------------------------------ +# Test the import of the settings file +#------------------------------------------------------------ try: from game import settings except Exception: @@ -114,10 +125,36 @@ except Exception: print string sys.exit(1) -# check required versions +#------------------------------------------------------------ +# Test runner setup +#------------------------------------------------------------ +os.environ['DJANGO_SETTINGS_MODULE'] = 'game.settings' +from django.test.simple import DjangoTestSuiteRunner +class EvenniaTestSuiteRunner(DjangoTestSuiteRunner): + """ + This test runner only runs tests on the apps specified in src/ and game/ to + avoid running the large number of tests defined by Django + """ + def build_suite(self, test_labels, extra_tests=None, **kwargs): + """ + Build a test suite for Evennia. test_labels is a list of apps to test. + If not given, a subset of settings.INSTALLED_APPS will be used. + """ + if not test_labels: + test_labels = [applabel.rsplit('.', 1)[1] for applabel in settings.INSTALLED_APPS + if (applabel.startswith('src.') or applabel.startswith('game.'))] + return super(EvenniaTestSuiteRunner, self).build_suite(test_labels, extra_tests=extra_tests, **kwargs) + def run_suite(self, test_labels=None, extra_tests=None, **kwargs): + "Run wrapper for the tests" + return super(EvenniaTestSuiteRunner, self).run_suite(self.build_suite(test_labels, extra_tests), **kwargs) + +#------------------------------------------------------------ +# This is run only if the module is called as a program +#------------------------------------------------------------ if __name__ == "__main__": - from django.core.management import execute_manager + + # checks if the settings file was created this run if _CREATED_SETTINGS: print """ Edit your new settings.py file as needed, then run @@ -125,7 +162,15 @@ if __name__ == "__main__": create the database and your superuser account. """ sys.exit() - # run the django setups - from src.utils.utils import check_evennia_dependencies + + # running the unit tests + if len(sys.argv) > 1 and sys.argv[1] == 'test-evennia': + print "Running Evennia-specific test suites ..." + EvenniaTestSuiteRunner(sys.argv[2:]).run_suite() + sys.exit() + + # run the standard django manager, if dependencies match + from src.utils.utils import check_evennia_dependencies if check_evennia_dependencies(): + from django.core.management import execute_manager execute_manager(settings) diff --git a/src/commands/cmdhandler.py b/src/commands/cmdhandler.py index afb6953e3d..cb787ef066 100644 --- a/src/commands/cmdhandler.py +++ b/src/commands/cmdhandler.py @@ -264,9 +264,15 @@ def format_multimatches(caller, matches): # Main command-handler function -def cmdhandler(caller, raw_string, unloggedin=False): +def cmdhandler(caller, raw_string, unloggedin=False, testing=False): """ This is the main function to handle any string sent to the engine. + + caller - calling object + raw_string - the command string given on the command line + unloggedin - if caller is an authenticated user or not + testing - if we should actually execute the command or not. + if True, the command instance will be returned instead. """ try: # catch bugs in cmdhandler itself try: # catch special-type commands @@ -375,7 +381,11 @@ def cmdhandler(caller, raw_string, unloggedin=False): # we make sure to validate its scripts. cmd.obj.scripts.validate() - # Parse and execute + if testing: + # only return the command instance + return cmd + + # Parse and execute cmd.parse() cmd.func() # Done! @@ -395,6 +405,10 @@ def cmdhandler(caller, raw_string, unloggedin=False): # cmd.obj is automatically made available. # we make sure to validate its scripts. cmd.obj.scripts.validate() + + if testing: + # only return the command instance + return syscmd # parse and run the command syscmd.parse() diff --git a/src/objects/tests.py b/src/objects/tests.py new file mode 100644 index 0000000000..557c0ad8de --- /dev/null +++ b/src/objects/tests.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- + +""" +Unit testing of the 'objects' Evennia component. + +Runs as part of the Evennia's test suite with 'manage.py test-evennia'. + +Please add new tests to this module as needed. + +Guidelines: + A 'test case' is testing a specific component and is defined as a class inheriting from unittest.TestCase. + The test case class can have a method setUp() that creates and sets up the testing environment. + All methods inside the test case class whose names start with 'test' are used as test methods by the runner. + Inside the test methods, special member methods assert*() are used to test the behaviour. +""" + +import re, time +try: + # this is a special optimized Django version, only available in current Django devel + from django.utils.unittest import TestCase +except ImportError: + # if our Django is older we use the normal version + # TODO: Switch this to django.test.TestCase when the but has been plugged that gives + # traceback when using that module over TransactionTestCase. + from django.test import TestCase + #from django.test import TransactionTestCase as TestCase +from django.conf import settings +from src.objects import models, objects +from src.utils import create +from src.server import session, sessionhandler + +class TestObjAttrs(TestCase): + """ + Test aspects of ObjAttributes + """ + def setUp(self): + "set up the test" + self.attr = models.ObjAttribute() + self.obj1 = create.create_object(objects.Object, key="testobj1", location=None) + self.obj2 = create.create_object(objects.Object, key="testobj2", location=self.obj1) + + # tests + def test_store_str(self): + hstring = "sdfv00=97sfjs842 ivfjlQKFos9GF^8dddsöäå-?%" + self.obj1.db.testattr = hstring + self.assertEqual(hstring, self.obj1.db.testattr) + def test_store_obj(self): + self.obj1.db.testattr = self.obj2 + self.assertEqual(self.obj2 ,self.obj1.db.testattr) + self.assertEqual(self.obj2.location, self.obj1.db.testattr.location) + + +#------------------------------------------------------------ +# Command testing +#------------------------------------------------------------ + +# print all feedback from test commands (can become very verbose!) +VERBOSE = False + +class FakeSession(session.SessionProtocol): + """ + A fake session that implements dummy versions of the real thing; this is needed to mimic + a logged-in player. + """ + def connectionMade(self): + self.prep_session() + sessionhandler.add_session(self) + def prep_session(self): + self.server, self.address = None, "0.0.0.0" + self.name, self.uid = None, None + self.logged_in = False + self.encoding = "utf-8" + self.cmd_last, self.cmd_last_visible, self.cmd_conn_time = time.time(), time.time(), time.time() + self.cmd_total = 0 + def disconnectClient(self): + pass + def lineReceived(self, raw_string): + pass + def msg(self, message, markup=True): + if VERBOSE: + print message + +class TestCommand(TestCase): + """ + Sets up the basics of testing the default commands and the generic things + that should always be present in a command. + + Inherit new tests from this. + """ + def setUp(self): + "sets up the testing environment" + self.room1 = create.create_object(settings.BASE_ROOM_TYPECLASS, key="room1") + self.room2 = create.create_object(settings.BASE_ROOM_TYPECLASS, key="room2") + + # create a faux player/character for testing. + self.char1 = create.create_player("TestingPlayer", "testplayer@test.com", "testpassword", location=self.room1) + self.char1.player.user.is_superuser = True + sess = FakeSession() + sess.connectionMade() + sess.login(self.char1.player) + + self.char2 = create.create_object(settings.BASE_CHARACTER_TYPECLASS, key="char2", location=self.room1) + self.obj1 = create.create_object(settings.BASE_OBJECT_TYPECLASS, key="obj1", location=self.room1) + self.obj2 = create.create_object(settings.BASE_OBJECT_TYPECLASS, key="obj2", location=self.room1) + self.exit1 = create.create_object(settings.BASE_EXIT_TYPECLASS, key="exit1", location=self.room1) + self.exit2 = create.create_object(settings.BASE_EXIT_TYPECLASS, key="exit2", location=self.room2) + + def get_cmd(self, cmd_class, argument_string=""): + """ + Obtain a cmd instance from a class and an input string + Note: This does not make use of the cmdhandler functionality. + """ + cmd = cmd_class() + cmd.caller = self.char1 + cmd.cmdstring = cmd_class.key + cmd.args = argument_string + cmd.cmdset = None + cmd.obj = self.char1 + return cmd + + def execute_cmd(self, raw_string): + """ + Creates the command through faking a normal command call; + This also mangles the input in various ways to test if the command + will be fooled. + """ + test1 = re.sub(r'\s', '', raw_string) # remove all whitespace inside it + test2 = "%s/åäö öäö;-:$£@*~^' 'test" % raw_string # inserting weird characters in call + test3 = "%s %s" % (raw_string, raw_string) # multiple calls + self.char1.execute_cmd(test1) + self.char1.execute_cmd(test2) + self.char1.execute_cmd(test3) + self.char1.execute_cmd(raw_string) + +#------------------------------------------------------------ +# Default set Command testing +#------------------------------------------------------------ + +class TestHome(TestCommand): + def test_call(self): + self.char1.home = self.room2 + self.execute_cmd("home") + self.assertEqual(self.char1.location, self.room2) +class TestLook(TestCommand): + def test_call(self): + self.execute_cmd("look here") +class TestPassword(TestCommand): + def test_call(self): + self.execute_cmd("@password testpassword = newpassword") +class TestNick(TestCommand): + def test_call(self): + self.execute_cmd("nickname testalias = testaliasedstring") + self.assertEquals("testaliasedstring", self.char1.nicks.get("testalias", None)) diff --git a/src/permissions/permissions.py b/src/permissions/permissions.py index c11a5e7afb..bee631f24e 100644 --- a/src/permissions/permissions.py +++ b/src/permissions/permissions.py @@ -538,7 +538,7 @@ def has_perm(accessing_obj, accessed_obj, lock_type, default_deny=False): if typelist and lock_type in typelist] if not locklist or not any(locklist): - # No locks; use default security policy + # No viable locks; use default security policy return not default_deny # we have locks of the right type. Set default flag OR on all that @@ -562,8 +562,7 @@ def has_perm(accessing_obj, accessed_obj, lock_type, default_deny=False): # try to add permissions from connected player if hasattr(accessing_obj, 'has_player') and accessing_obj.has_player: - # accessing_obj has a valid, connected player. We start with - # those permissions. + # accessing_obj has a valid, connected player. We start with those permissions. player = accessing_obj.player if player.is_superuser: # superuser always has access diff --git a/src/utils/create.py b/src/utils/create.py index 79a02c231d..e535fdaa85 100644 --- a/src/utils/create.py +++ b/src/utils/create.py @@ -417,7 +417,7 @@ def create_player(name, email, password, new_user = User.objects.create_user(name, email, password) # create the associated Player for this User, and tie them together - new_player = PlayerDB(db_key=name, user=new_user) + new_player = PlayerDB(db_key=name, user=new_user, db_typeclass_path=typeclass) new_player.save() # assign mud permissions diff --git a/src/utils/reloads.py b/src/utils/reloads.py index 78c56c9368..2643be9f60 100644 --- a/src/utils/reloads.py +++ b/src/utils/reloads.py @@ -133,7 +133,8 @@ def cemit_info(message): infochan = Channel.objects.get_channel(infochan[0]) except Exception: return - cname = infochan.key - cmessage = "\n".join(["[%s]: %s" % (cname, line) for line in message.split('\n')]) - infochan.msg(cmessage) + if infochan: + cname = infochan.key + cmessage = "\n".join(["[%s]: %s" % (cname, line) for line in message.split('\n')]) + infochan.msg(cmessage)