# -*- test-case-name: mv3d.test.server -*- # Copyright (C) 2007-2012 Mortal Coil Games # See LICENSE for details. """ A simple server/client implementation to allow for tests """ import sys from ConfigParser import ConfigParser from StringIO import StringIO from zope.interface import implements #@UnresolvedImport from twisted.application import service from twisted.internet import reactor from twisted.internet.defer import inlineCallbacks, Deferred, returnValue from twisted.spread import pb from twisted.cred import checkers, portal, credentials from twisted.cred.error import UnauthorizedLogin from twisted.trial.unittest import TestCase from mv3d.util.classgen import ClassGenerator from mv3d.server.sim import SimCluster from mv3d.server.model.octree import OctreeArea from mv3d.util.conductor import Conductor from mv3d.server.iserver import IDirectoryService, IRealmService, IAssetService, \ ISimulationService, IAccountService import os import mv3d from mv3d.resource.url import newURLImageAsset rootPath = os.path.abspath(os.path.join(os.path.dirname(mv3d.__file__), "..")) fullServerConfig = """ [Test] serviceNames=Dir, Account, Asset, Realm, Player, Sim, Login, PBServer, HttpsServer connectionFactories=PBClient, HTTPClient name=MV3D Server logLevel=10 directoryServices=%(dirServices)s [Sim] type=mv3d.server.sim.SimulationService grantPermissions=read:all publicLocation=pb://localhost:%(pbport)s/Sim realmServers=%(realmServices)s storeInterval=15 interfaces=mv3d.server.sim.SimServiceView [Dir] type=mv3d.server.directory.DirectoryService grantPermissions=read:all publicLocation=pb://localhost:%(pbport)s/Dir interfaces=mv3d.server.directory.DirectoryServiceView [Realm] type=mv3d.server.realm.RealmService grantPermissions=read:all directoryServers=%(dirServices)s publicLocation=pb://localhost:%(pbport)s/Realm interfaces=mv3d.server.realm.RealmServiceView [Asset] type=mv3d.server.asset.AssetService grantPermissions=read:all, reference:all interfaces=mv3d.server.asset.AssetServiceView directoryServers=%(dirServices)s publicLocation=pb://localhost:%(pbport)s/Asset [Account] type=mv3d.server.account.AccountService interfaces=mv3d.server.account.AccountServiceView [Login] type=mv3d.server.login.LoginService accountServices=%(acctServices)s site=HttpsServer docRoot=/ templateDir=""" + rootPath + """/templates/loginservice allowNewAccounts=false [Player] type=mv3d.server.player.PlayerService grantPermissions=read:all, reference:all simulators=%(simulationServices)s interfaces=mv3d.server.player.PlayerServiceView publicLocation=pb://localhost:%(pbport)s/Player [PBServer] type=mv3d.server.network.PBServer port=%(pbport)s authenticators=%(auths)s publish=Player, Asset, Account, Sim, Player, Realm, Dir, Login insecureLocalAccounts=test:pass:admin [PBClient] type=mv3d.net.client.PBConFactory defaultCredentials=test,local:pass localAliases=pb://localhost:%(pbport)s/ loginServices=%(loginSvcs)s [HttpsServer] type=mv3d.server.network.HttpServer port=%(httpport)s ssl=off publish=Login allowAnonymous=Login authenticators=%(auths)s static=static:media/staticweb [HTTPClient] type=mv3d.net.client.JSONConFactory protocol=http """ simServerConfig = """ [Test] serviceNames=Asset, Sim, PBServer [PBServer] publish=Asset, Sim """ useStoresConfig = """ [Sim] store=%(storeDir)s/Sim [Dir] store=%(storeDir)s/Dir [Realm] store=%(storeDir)s/Realm [Asset] store=%(storeDir)s/Asset [Account] store=%(storeDir)s/Account """ class SimpleServer(pb.Root): gotObject = None listener = None def __init__(self, port=8800): self.port = port def remote_giveObject(self, obj): self.gotObject = obj def listen(self): self.application = service.Application("test_server") self.listener = reactor.listenTCP(self.port, #@UndefinedVariable pb.PBServerFactory(self)) def stopListening(self): return self.listener.stopListening() class SimplePersp(pb.Avatar): gotObject = None controller = None def __init__(self, server): self.parent = server self.server = server def broadcastEvent(self, *_args, **_keys): """ ignored for this test """ def perspective_giveObject(self, obj): """ accept an object as the arg of this method """ self.server.gotObject = obj def perspective_getObject(self): """ return our object """ return self.server.gotObject class ServerWithAuth(object): """ A simple server with basic authentication mostly used for testing pb things which require an avatar. """ persp = None listener = None avatarClass = SimplePersp connectionCount = 0 password = "pass" gotObject = None parent = None implements(portal.IRealm, checkers.ICredentialsChecker) credentialInterfaces = (credentials.IUsernameHashedPassword,) def __init__(self, port=8800): self.persp = [] self.log = [] self.connections = [] self.port = port def requestAvatar(self, _avatarId, mind, *_interfaces): self.persp.append(self.avatarClass(self)) self.connectionCount += 1 self.connections.append(mind) self.persp[-1].controller = mind return pb.IPerspective, self.persp[-1], lambda:self.connections.remove( mind) def requestAvatarId(self, credentials): self.log.append(("requestAvatarId", credentials)) if not credentials.checkPassword(self.password): raise UnauthorizedLogin("Boo hoo") return credentials.username.split(",")[0] def listen(self): p = portal.Portal(self) p.registerChecker(self) self.listener = reactor.listenTCP(#@UndefinedVariable self.port, pb.PBServerFactory(p)) def stopListening(self): return self.listener.stopListening() class SimpleClient: def __init__(self, port=8800): self.port = port def connect(self, _mind=None): factory = pb.PBClientFactory() reactor.connectTCP("localhost", self.port, factory) #@UndefinedVariable d = factory.getRootObject() def gotRoot(root): self.remote = root d.addCallback(gotRoot) return d def disconnect(self): return self.remote.broker.transport.loseConnection() def sendObject(self, obj): return self.remote.callRemote("giveObject", obj) class ClientWithAuth: def __init__(self, port=8800): self.port = port def connect(self, mind=None): factory = pb.PBClientFactory() reactor.connectTCP("localhost", self.port, factory) #@UndefinedVariable d = factory.login(credentials.UsernamePassword( "test", "pass"), mind) def gotRoot(root): self.remote = root d.addCallback(gotRoot) return d def disconnect(self): return self.remote.broker.transport.loseConnection() def sendObject(self, obj): return self.remote.callRemote("giveObject", obj) def getObject(self): return self.remote.callRemote("getObject") class SimpleCopyable(pb.Copyable, pb.RemoteCopy): name = "hi" test = 1 pb.setUnjellyableForClass(SimpleCopyable, SimpleCopyable) class WithSimpleServer(object): serverType = SimpleServer clientType = SimpleClient def setUp(self): self.server = self.serverType() self.server.listen() @inlineCallbacks def tearDown(self): yield self.server.stopListening() # Sometimes it takes an extra bit of time to really close the # connections. dfrd = Deferred() reactor.callLater(0.1, dfrd.callback, None) #@UndefinedVariable yield dfrd def connect(self, mind=None): """ Connect a client to the server and return it """ scl = self.clientType() dfrd = scl.connect(mind) return dfrd.addCallback(lambda _: scl) class SimpleServerTests(TestCase): """ Test the simple server """ def test_listen(self): """ Test listening """ s = SimpleServer() s.listen() return s.stopListening() class ServerWithAuthTests(TestCase): """ Test the simple server """ def test_listen(self): """ Test listening """ s = ServerWithAuth() s.listen() return s.stopListening() class SimpleClientTests(WithSimpleServer, TestCase): """ Test the simple client """ def test_connect(self): """ Test connecting to the server """ sc = SimpleClient() d = sc.connect() def connected(_ignored): return sc.disconnect() d.addCallback(connected) return d def test_sendObj(self): """ Test sending an object over the connection """ sc = SimpleClient() d = sc.connect() def connected(_ignored): cp = SimpleCopyable() cp.friend = 23 d = sc.sendObject(cp) def gotIt(_ignored): obj = self.server.gotObject self.assertEqual(obj.name, "hi") self.assertEqual(obj.test, 1) self.assertEqual(obj.friend, 23) return sc.disconnect() d.addCallback(gotIt) return d d.addCallback(connected) return d class ClientWithAuthTests(WithSimpleServer, TestCase): """ Test the simple client """ serverType = ServerWithAuth def test_connect(self): """ Test connecting to the server """ sc = ClientWithAuth() d = sc.connect() def connected(_ignored): return sc.disconnect() d.addCallback(connected) return d def test_sendObj(self): """ Test sending an object over the connection """ sc = ClientWithAuth() d = sc.connect() def connected(_ignored): cp = SimpleCopyable() cp.friend = 23 d = sc.sendObject(cp) def gotIt(_ignored): obj = self.server.gotObject self.assertEqual(obj.name, "hi") self.assertEqual(obj.test, 1) self.assertEqual(obj.friend, 23) return sc.disconnect() d.addCallback(gotIt) return d d.addCallback(connected) return d class ServerData(object): """ Stores data about a particular server. """ class WithRealServer(TestCase): """ Instantiates a real server instance complete with area and object. """ serverConfig = [("", dict( pbport="8282", httpport="8181", loginSvcs="self/Login", auths="self/Account", realmServices="self/Realm", dirServices="self/Dir", acctServices="self/Account", simulationServices="pb://localhost:8282/Sim"))] @inlineCallbacks def createRealm(self, rsvc, server): """ Create a new realm on the specified service and return the id """ rid = yield rsvc.newMasterRealm( ClassGenerator("mv3d.server.model.realm.odeRealm")) server.realm = yield rsvc.getRealm(rid) server.realm.setPhysicsProperty("gravity", (0, -9.81, 0)) server.realmHolder = rsvc.getPool(server.realm.clusterID) if self.realm is None: self.realm = server.realm if self.realmHolder is None: self.realmHolder = server.realmHolder returnValue(rid) @inlineCallbacks def createArea(self, realmID, rsvc, sim, pool): """ Create a new area and return the id """ aid = yield rsvc.newItem(realmID, pool.uid, pool.getMembers()) area = OctreeArea(sim.publicLocation, aid) sim.addPool(area) pool.addMasterPool(rsvc.parent, [sim.publicLocation], aid) yield pool.updateMechanism("addItem", aid, aid) yield area.create(sim, pool.uid, (0, 0, 0), 1000) if self.area is None: self.area = area area.stopUpdates() returnValue(aid) @inlineCallbacks def createBox(self, area, sim, position=(10, 0, 10), inactive=False): """ Create a box at the specified position. Return the item id """ iid = yield area.newItem(position, ClassGenerator("mv3d.server.model.physical.PhysicalBox")) obj = sim.items[iid] obj.setPosition(position) obj.addManipulatorClass("Player Manipulator", ClassGenerator("mv3d.test.test_clientfaker.FakePlayerManip")) if self.object is None: self.object = obj self.objects.append(obj) if inactive: yield obj.deactivate() returnValue(obj.getID()) @inlineCallbacks def createAssetGroup(self, assetSvc): """ Create a new asset group and return the id. """ grpid = yield assetSvc.newMasterAssetGroup(ClassGenerator( "mv3d.server.asset.AssetGroup")) assetGroup = yield self.assetSvc.getAssetGroup(grpid) if self.assetGroup is None: self.assetGroup = assetGroup self.asset = yield newURLImageAsset(assetSvc.parent, assetGroup.group, "http://foo.com", "localfile", "foo") returnValue(grpid) @inlineCallbacks def createAccount(self, asvc, user="test", password="user", playerObject=None): """ Create an account and optionally assign a player object to it. """ asvc.createAccount(user, password, "t", ["admin"]) account = yield asvc.getAccount("test") if playerObject is not None: account.addPC(playerObject) returnValue(account) @inlineCallbacks def createServer(self, config, replacements, createStuff=True): """ Spin up a server. """ server = ServerData() cfg = ConfigParser(replacements) cfg.readfp(StringIO(fullServerConfig)) server.options = dict(storeDir=self.mktemp()) if not os.path.exists(server.options["storeDir"]): os.makedirs(server.options["storeDir"]) cfg.readfp(StringIO(config % server.options)) conductor = Conductor() conductor.configure("Test", cfg) conductor.dirServices = replacements["dirServices"] yield conductor.startService() dsvc = conductor.getLocalService(IDirectoryService, None) rsvc = conductor.getLocalService(IRealmService, None) sim = conductor.getLocalService(ISimulationService, None) server.assetSvc = conductor.getLocalService(IAssetService, None) asvc = conductor.getLocalService(IAccountService, None) server.conductor = conductor if self.server is None: self.server = conductor server.rsvc = rsvc if self.rsvc is None: self.rsvc = rsvc server.asvc = asvc if self.asvc is None: self.asvc = asvc server.sim = sim if self.sim is None: self.sim = sim self.servers.append(server) if not createStuff: returnValue(None) if dsvc is not None: self.assertNotIdentical(dsvc.local, None) yield dsvc.newMasterDirectory("Realms") yield dsvc.newMasterDirectory("AssetGroups") if rsvc is not None: rid = yield self.createRealm(rsvc, server) if sim is not None: server.simCluster = self.createSimCluster(sim) if rsvc is not None: aid = yield self.createArea(rid, rsvc, sim, server.simCluster) area = sim.getPool(aid) server.area = area iid = yield self.createBox(area, sim, (0, 0, 0)) server.object = sim.items[iid] if self.assetSvc is None: self.assetSvc = server.assetSvc if server.assetSvc is not None and self.assetGroup is None: yield self.createAssetGroup(server.assetSvc) if asvc is not None: self.account = yield self.createAccount(asvc, playerObject=iid) def createSimCluster(self, sim): """ Create a new sim cluster """ pool = SimCluster(sim.publicLocation) if self.simCluster is None: self.simCluster = pool sim.addPool(pool) return pool @inlineCallbacks def setUp(self): """ Create """ self.server = None self.realm = None self.realmHolder = None self.rsvc = None self.asvc = None self.sim = None self.assetSvc = None self.assetGroup = None self.simCluster = None self.area = None self.object = None self.servers = [] self.objects = [] for config, replacements in self.serverConfig: yield self.createServer(config, replacements) @inlineCallbacks def tearDown(self): """ Stop all the servers """ for server in reversed(self.servers): if server.conductor.running: if server.sim is not None: for item in server.sim.items.values(): if hasattr(item, "_waitingForParent"): if item._waitingForParent is not None: item._waitingForParent.cancel yield server.conductor.stopService() for object in self.objects: if object._waitingForParent is not None: object._waitingForParent.cancel() def assertNoOgre(self): """ Assert that ogre hasn't been imported """ m = sys.modules.keys() ogre = filter(lambda x: x.startswith("ogre"), m) self.assertEqual(ogre, [], "Ogre was imported")