Moved dummyrunner into a separate directory under src/utils
This commit is contained in:
parent
d5c1d35406
commit
047de533f3
3 changed files with 6 additions and 0 deletions
6
src/utils/dummyrunner/README.txt
Normal file
6
src/utils/dummyrunner/README.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
Dummyrunner
|
||||
|
||||
This is a test system for stress-testing the server. It will launch numbers
|
||||
of "dummy players" to connect to the server and do various sequences of actions.
|
||||
See header of dummyrunner.py for usage.
|
||||
289
src/utils/dummyrunner/dummyrunner.py
Normal file
289
src/utils/dummyrunner/dummyrunner.py
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
"""
|
||||
Dummy client runner
|
||||
|
||||
This module implements a stand-alone launcher for stress-testing
|
||||
an Evennia game. It will launch any number of fake clients. These
|
||||
clients will log into the server and start doing random operations.
|
||||
Customizing and weighing these operations differently depends on
|
||||
which type of game is tested. The module contains a testing module
|
||||
for plain Evennia.
|
||||
|
||||
Please note that you shouldn't run this on a production server!
|
||||
Launch the program without any arguments or options to see a
|
||||
full step-by-step setup help.
|
||||
|
||||
Basically (for testing default Evennia):
|
||||
|
||||
- Use an empty/testing database.
|
||||
- set PERMISSION_PLAYER_DEFAULT = "Builders"
|
||||
- start server, eventually with profiling active
|
||||
- launch this client runner
|
||||
|
||||
If you want to customize the runner's client actions
|
||||
(because you changed the cmdset or needs to better
|
||||
match your use cases or add more actions), you can
|
||||
change which actions by adding a path to
|
||||
|
||||
DUMMYRUNNER_ACTIONS_MODULE = <path.to.your.module>
|
||||
|
||||
in your settings. See utils.dummyrunner_actions.py
|
||||
for instructions on how to define this module.
|
||||
|
||||
"""
|
||||
|
||||
import os, sys, time, random
|
||||
from optparse import OptionParser
|
||||
from twisted.conch import telnet
|
||||
from twisted.internet import reactor, protocol
|
||||
# from twisted.application import internet, service
|
||||
# from twisted.web import client
|
||||
from twisted.internet.task import LoopingCall
|
||||
|
||||
# Tack on the root evennia directory to the python path and initialize django settings
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
from django.core.management import setup_environ
|
||||
from game import settings
|
||||
setup_environ(settings)
|
||||
|
||||
from django.conf import settings
|
||||
from src.utils import utils
|
||||
|
||||
HELPTEXT = """
|
||||
|
||||
Usage: dummyrunner.py [-h][-v][-V] [nclients]
|
||||
|
||||
DO NOT RUN THIS ON A PRODUCTION SERVER! USE A CLEAN/TESTING DATABASE!
|
||||
|
||||
This stand-alone program launches dummy telnet clients against a
|
||||
running Evennia server. The idea is to mimic real players logging in
|
||||
and repeatedly doing resource-heavy commands so as to stress test the
|
||||
game. It uses the default command set to log in and issue commands, so
|
||||
if that was customized, some of the functionality will not be tested
|
||||
(it will not fail, the commands will just not be recognized). The
|
||||
running clients will create new objects and rooms all over the place
|
||||
as part of their running, so using a clean/testing database is
|
||||
strongly recommended.
|
||||
|
||||
Setup:
|
||||
1) setup a fresh/clean database (if using sqlite, just safe-copy
|
||||
away your real evennia.db3 file and create a new one with
|
||||
manage.py)
|
||||
2) in game/settings.py, add
|
||||
|
||||
PERMISSION_PLAYER_DEFAULT="Builders"
|
||||
|
||||
3a) Start Evennia like normal.
|
||||
3b) If you want profiling, start Evennia like this instead:
|
||||
|
||||
python runner.py -S start
|
||||
|
||||
this will start Evennia under cProfiler with output server.prof.
|
||||
4) run this dummy runner:
|
||||
|
||||
python dummyclients.py <nr_of_clients> [timestep] [port]
|
||||
|
||||
Default is to connect one client to port 4000, using a 5 second
|
||||
timestep. Increase the number of clients and shorten the
|
||||
timestep (minimum is 1s) to further stress the game.
|
||||
|
||||
You can stop the dummy runner with Ctrl-C.
|
||||
|
||||
5) Log on and determine if game remains responsive despite the
|
||||
heavier load. Note that if you do profiling, there is an
|
||||
additional overhead from the profiler too!
|
||||
6) If you use profiling, let the game run long enough to gather
|
||||
data, then stop the server. You can inspect the server.prof file
|
||||
from a python prompt (see Python's manual on cProfiler).
|
||||
|
||||
"""
|
||||
# number of clients to launch if no input is given on command line
|
||||
DEFAULT_NCLIENTS = 1
|
||||
# time between each 'tick', in seconds, if not set on command
|
||||
# line. All launched clients will be called upon to possibly do an
|
||||
# action with this frequency.
|
||||
DEFAULT_TIMESTEP = 5
|
||||
# Port to use, if not specified on command line
|
||||
DEFAULT_PORT = settings.TELNET_PORTS[0]
|
||||
# chance of an action happening, per timestep. This helps to
|
||||
# spread out usage randomly, like it would be in reality.
|
||||
CHANCE_OF_ACTION = 0.1
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Helper functions
|
||||
#------------------------------------------------------------
|
||||
|
||||
def idcounter():
|
||||
"generates subsequent id numbers"
|
||||
idcount = 0
|
||||
while True:
|
||||
idcount += 1
|
||||
yield idcount
|
||||
OID = idcounter()
|
||||
CID = idcounter()
|
||||
|
||||
def makeiter(obj):
|
||||
"makes everything iterable"
|
||||
if not hasattr(obj, '__iter__'):
|
||||
return [obj]
|
||||
return obj
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Client classes
|
||||
#------------------------------------------------------------
|
||||
|
||||
class DummyClient(telnet.StatefulTelnetProtocol):
|
||||
"""
|
||||
Handles connection to a running Evennia server,
|
||||
mimicking a real player by sending commands on
|
||||
a timer.
|
||||
"""
|
||||
|
||||
def connectionMade(self):
|
||||
|
||||
# public properties
|
||||
self.cid = CID.next()
|
||||
self.istep = 0
|
||||
self.exits = [] # exit names created
|
||||
self.objs = [] # obj names created
|
||||
|
||||
self._report = ""
|
||||
self._cmdlist = [] # already stepping in a cmd definition
|
||||
self._ncmds = 0
|
||||
self._actions = self.factory.actions
|
||||
self._echo_brief = self.factory.verbose == 1
|
||||
self._echo_all = self.factory.verbose == 2
|
||||
#print " ** client %i connected." % self.cid
|
||||
|
||||
reactor.addSystemEventTrigger('before', 'shutdown', self.logout)
|
||||
|
||||
# start client tick
|
||||
d = LoopingCall(self.step)
|
||||
d.start(self.factory.timestep, now=True).addErrback(self.error)
|
||||
|
||||
def dataReceived(self, data):
|
||||
"Echo incoming data to stdout"
|
||||
if self._echo_all:
|
||||
print data
|
||||
|
||||
def connectionLost(self, reason):
|
||||
"loosing the connection"
|
||||
#print " ** client %i lost connection." % self.cid
|
||||
|
||||
def error(self, err):
|
||||
"error callback"
|
||||
print err
|
||||
|
||||
def counter(self):
|
||||
"produces a unique id, also between clients"
|
||||
return OID.next()
|
||||
|
||||
def logout(self):
|
||||
"Causes the client to log out of the server. Triggered by ctrl-c signal."
|
||||
cmd, report = self._actions[1](self)
|
||||
print "client %i %s (%s actions)" % (self.cid, report, self.istep)
|
||||
self.sendLine(cmd)
|
||||
|
||||
def step(self):
|
||||
"""
|
||||
Perform a step. This is called repeatedly by the runner
|
||||
and causes the client to issue commands to the server.
|
||||
This holds all "intelligence" of the dummy client.
|
||||
"""
|
||||
if random.random() > CHANCE_OF_ACTION:
|
||||
return
|
||||
if not self._cmdlist:
|
||||
# no cmdlist in store, get a new one
|
||||
if self.istep == 0:
|
||||
cfunc = self._actions[0]
|
||||
else: # random selection using cumulative probabilities
|
||||
rand = random.random()
|
||||
cfunc = [func for cprob, func in self._actions[2] if cprob >= rand][0]
|
||||
# assign to internal cmdlist
|
||||
cmd, self._report = cfunc(self)
|
||||
self._cmdlist = list(makeiter(cmd))
|
||||
self._ncmds = len(self._cmdlist)
|
||||
# output
|
||||
if self.istep == 0 and not (self._echo_brief or self._echo_all):
|
||||
print "client %i %s" % (self.cid, self._report)
|
||||
elif self.istep == 0 or self._echo_brief or self._echo_all:
|
||||
print "client %i %s (%i/%i)" % (self.cid, self._report, self._ncmds-(len(self._cmdlist)-1), self._ncmds)
|
||||
# launch the action by popping the first element from cmdlist (don't hide tracebacks)
|
||||
self.sendLine(str(self._cmdlist.pop(0)))
|
||||
self.istep += 1 # only steps up if an action is taken
|
||||
|
||||
class DummyFactory(protocol.ClientFactory):
|
||||
protocol = DummyClient
|
||||
def __init__(self, actions, timestep, verbose):
|
||||
"Setup the factory base (shared by all clients)"
|
||||
self.actions = actions
|
||||
self.timestep = timestep
|
||||
self.verbose = verbose
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Access method:
|
||||
# Starts clients and connects them to a running server.
|
||||
#------------------------------------------------------------
|
||||
|
||||
def start_all_dummy_clients(actions, nclients=1, timestep=5, telnet_port=4000, verbose=0):
|
||||
|
||||
# validating and preparing the action tuple
|
||||
|
||||
# make sure the probabilities add up to 1
|
||||
pratio = 1.0 / sum(tup[0] for tup in actions[2:])
|
||||
flogin, flogout, probs, cfuncs = actions[0], actions[1], [tup[0] * pratio for tup in actions[2:]], [tup[1] for tup in actions[2:]]
|
||||
# create cumulative probabilies for the random actions
|
||||
cprobs = [sum(v for i,v in enumerate(probs) if i<=k) for k in range(len(probs))]
|
||||
# rebuild a new, optimized action structure
|
||||
actions = (flogin, flogout, zip(cprobs, cfuncs))
|
||||
|
||||
# setting up all clients (they are automatically started)
|
||||
factory = DummyFactory(actions, timestep, verbose)
|
||||
for i in range(nclients):
|
||||
reactor.connectTCP("localhost", telnet_port, factory)
|
||||
# start reactor
|
||||
reactor.run()
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Command line interface
|
||||
#------------------------------------------------------------
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
# parsing command line with default vals
|
||||
parser = OptionParser(usage="%prog [options] <nclients> [timestep, [port]]",
|
||||
description="This program requires some preparations to run properly. Start it without any arguments or options for full help.")
|
||||
parser.add_option('-v', '--verbose', action='store_const', const=1, dest='verbose',
|
||||
default=0,help="echo brief description of what clients do every timestep.")
|
||||
parser.add_option('-V', '--very-verbose', action='store_const',const=2, dest='verbose',
|
||||
default=0,help="echo all client returns to stdout (hint: use only with nclients=1!)")
|
||||
|
||||
options, args = parser.parse_args()
|
||||
|
||||
nargs = len(args)
|
||||
nclients = DEFAULT_NCLIENTS
|
||||
timestep = DEFAULT_TIMESTEP
|
||||
port = DEFAULT_PORT
|
||||
try:
|
||||
if not args : raise Exception
|
||||
if nargs > 0: nclients = max(1, int(args[0]))
|
||||
if nargs > 1: timestep = max(1, int(args[1]))
|
||||
if nargs > 2: port = int(args[2])
|
||||
except Exception:
|
||||
print HELPTEXT
|
||||
sys.exit()
|
||||
|
||||
# import the ACTION tuple from a given module
|
||||
try:
|
||||
action_modpath = settings.DUMMYRUNNER_ACTIONS_MODULE
|
||||
except AttributeError:
|
||||
# use default
|
||||
action_modpath = "src.utils.dummyrunner_actions"
|
||||
actions = utils.variable_from_module(action_modpath, "ACTIONS")
|
||||
|
||||
print "Connecting %i dummy client(s) to port %i using a %i second timestep ... " % (nclients, port, timestep)
|
||||
t0 = time.time()
|
||||
start_all_dummy_clients(actions, nclients, timestep, port,
|
||||
verbose=options.verbose)
|
||||
ttot = time.time() - t0
|
||||
print "... dummy client runner finished after %i seconds." % ttot
|
||||
200
src/utils/dummyrunner/dummyrunner_actions.py
Normal file
200
src/utils/dummyrunner/dummyrunner_actions.py
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
"""
|
||||
These are actions for the dummy client runner, using
|
||||
the default command set and intended for unmodified Evennia.
|
||||
|
||||
Each client action is defined as a function. The clients
|
||||
will perform these actions randomly (except the login action).
|
||||
|
||||
Each action-definition function should take one argument- "client",
|
||||
which is a reference to the client currently performing the action
|
||||
Use the client object for saving data between actions.
|
||||
|
||||
The client object has the following relevant properties and methods:
|
||||
cid - unique client id
|
||||
istep - the current step
|
||||
exits - an empty list. Can be used to store exit names
|
||||
objs - an empty list. Can be used to store object names
|
||||
counter() - get an integer value. This counts up for every call and
|
||||
is always unique between clients.
|
||||
|
||||
The action-definition function should return the command that the
|
||||
client should send to the server (as if it was input in a mud client).
|
||||
It should also return a string detailing the action taken. This string is
|
||||
used by the "brief verbose" mode of the runner and is prepended by
|
||||
"Client N " to produce output like "Client 3 is creating objects ..."
|
||||
|
||||
This module *must* also define a variable named ACTIONS. This is a tuple
|
||||
where the first element is the function object for the action function
|
||||
to call when the client logs onto the server. The following elements
|
||||
are 2-tuples (probability, action_func), where probability defines how
|
||||
common it is for that particular action to happen. The runner will
|
||||
randomly pick between those functions based on the probability.
|
||||
|
||||
ACTIONS = (login_func, (0.3, func1), (0.1, func2) ... )
|
||||
|
||||
To change the runner to use your custom ACTION and/or action
|
||||
definitions, edit settings.py and add
|
||||
|
||||
DUMMYRUNNER_ACTIONS_MODULE = "path.to.your.module"
|
||||
|
||||
"""
|
||||
|
||||
# it's very useful to have a unique id for this run to avoid any risk
|
||||
# of clashes
|
||||
|
||||
import time
|
||||
RUNID = time.time()
|
||||
|
||||
# some convenient templates
|
||||
|
||||
START_ROOM = "testing_room_start-%s-%s" % (RUNID, "%i")
|
||||
ROOM_TEMPLATE = "testing_room_%s-%s" % (RUNID, "%i")
|
||||
EXIT_TEMPLATE = "exit_%s-%s" % (RUNID, "%i")
|
||||
OBJ_TEMPLATE = "testing_obj_%s-%s" % (RUNID, "%i")
|
||||
TOBJ_TEMPLATE = "testing_button_%s-%s" % (RUNID, "%i")
|
||||
TOBJ_TYPECLASS = "examples.red_button.RedButton"
|
||||
|
||||
# action function definitions
|
||||
|
||||
def c_login(client):
|
||||
"logins to the game"
|
||||
cname = "Dummy-%s-%i" % (RUNID, client.cid)
|
||||
#cemail = "%s@dummy.com" % (cname.lower())
|
||||
cpwd = "%s-%s" % (RUNID, client.cid)
|
||||
# set up for digging a first room (to move to)
|
||||
roomname = ROOM_TEMPLATE % client.counter()
|
||||
exitname1 = EXIT_TEMPLATE % client.counter()
|
||||
exitname2 = EXIT_TEMPLATE % client.counter()
|
||||
client.exits.extend([exitname1, exitname2])
|
||||
cmd = '@dig %s = %s, %s' % (roomname, exitname1, exitname2)
|
||||
cmd = ('create %s %s' % (cname, cpwd),
|
||||
'connect %s %s' % (cname, cpwd),
|
||||
'@dig %s' % START_ROOM % client.cid,
|
||||
'@teleport %s' % START_ROOM % client.cid,
|
||||
'@dig %s = %s, %s' % (roomname, exitname1, exitname2)
|
||||
)
|
||||
|
||||
return cmd, "logs in as %s ..." % cname
|
||||
|
||||
def c_logout(client):
|
||||
"logouts of the game"
|
||||
return "@quit", "logs out"
|
||||
|
||||
def c_looks(client):
|
||||
"looks at various objects"
|
||||
cmd = ["look %s" % obj for obj in client.objs]
|
||||
if not cmd:
|
||||
cmd = ["look %s" % exi for exi in client.exits]
|
||||
if not cmd:
|
||||
cmd = "look"
|
||||
return cmd, "looks ..."
|
||||
|
||||
def c_examines(client):
|
||||
"examines various objects"
|
||||
cmd = ["examine %s" % obj for obj in client.objs]
|
||||
if not cmd:
|
||||
cmd = ["examine %s" % exi for exi in client.exits]
|
||||
if not cmd:
|
||||
cmd = "examine me"
|
||||
return cmd, "examines objs ..."
|
||||
|
||||
def c_help(client):
|
||||
"reads help files"
|
||||
cmd = ('help',
|
||||
'help @teleport',
|
||||
'help look',
|
||||
'help @tunnel',
|
||||
'help @dig')
|
||||
return cmd, "reads help ..."
|
||||
|
||||
def c_digs(client):
|
||||
"digs a new room, storing exit names on client"
|
||||
roomname = ROOM_TEMPLATE % client.counter()
|
||||
exitname1 = EXIT_TEMPLATE % client.counter()
|
||||
exitname2 = EXIT_TEMPLATE % client.counter()
|
||||
client.exits.extend([exitname1, exitname2])
|
||||
cmd = '@dig %s = %s, %s' % (roomname, exitname1, exitname2)
|
||||
return cmd, "digs ..."
|
||||
|
||||
def c_creates_obj(client):
|
||||
"creates normal objects, storing their name on client"
|
||||
objname = OBJ_TEMPLATE % client.counter()
|
||||
client.objs.append(objname)
|
||||
cmd = ('@create %s' % objname,
|
||||
'@desc %s = "this is a test object' % objname,
|
||||
'@set %s/testattr = this is a test attribute value.' % objname,
|
||||
'@set %s/testattr2 = this is a second test attribute.' % objname)
|
||||
return cmd, "creates obj ..."
|
||||
|
||||
def c_creates_button(client):
|
||||
"creates example button, storing name on client"
|
||||
objname = TOBJ_TEMPLATE % client.counter()
|
||||
client.objs.append(objname)
|
||||
cmd = ('@create %s:%s' % (objname, TOBJ_TYPECLASS),
|
||||
'@desc %s = test red button!' % objname)
|
||||
return cmd, "creates button ..."
|
||||
|
||||
def c_socialize(client):
|
||||
"socializechats on channel"
|
||||
cmd = ('ooc Hello!',
|
||||
'ooc Testing ...',
|
||||
'ooc Testing ... times 2',
|
||||
'say Yo!',
|
||||
'emote stands looking around.')
|
||||
return cmd, "socializes ..."
|
||||
|
||||
def c_moves(client):
|
||||
"moves to a previously created room, using the stored exits"
|
||||
cmd = client.exits # try all exits - finally one will work
|
||||
if not cmd: cmd = "look"
|
||||
return cmd, "moves ..."
|
||||
|
||||
|
||||
# Action tuple (required)
|
||||
#
|
||||
# This is a tuple of client action functions. The first element is the
|
||||
# function the client should use to log into the game and move to
|
||||
# STARTROOM . The second element is the logout command, for cleanly
|
||||
# exiting the mud. The following elements are 2-tuples of (probability,
|
||||
# action_function). The probablities should normally sum up to 1,
|
||||
# otherwise the system will normalize them.
|
||||
#
|
||||
|
||||
## "normal builder" definition
|
||||
#ACTIONS = ( c_login,
|
||||
# c_logout,
|
||||
# (0.5, c_looks),
|
||||
# (0.08, c_examines),
|
||||
# (0.1, c_help),
|
||||
# (0.01, c_digs),
|
||||
# (0.01, c_creates_obj),
|
||||
# #(0.1, c_creates_button),
|
||||
# (0.3, c_moves))
|
||||
## "heavy" builder definition
|
||||
#ACTIONS = ( c_login,
|
||||
# c_logout,
|
||||
# (0.2, c_looks),
|
||||
# (0.1, c_examines),
|
||||
# (0.2, c_help),
|
||||
# (0.1, c_digs),
|
||||
# (0.1, c_creates_obj),
|
||||
# #(0.01, c_creates_button),
|
||||
# (0.2, c_moves))
|
||||
## "passive player" definition
|
||||
#ACTIONS = ( c_login,
|
||||
# c_logout,
|
||||
# (0.7, c_looks),
|
||||
# #(0.1, c_examines),
|
||||
# (0.3, c_help))
|
||||
# #(0.1, c_digs),
|
||||
# #(0.1, c_creates_obj),
|
||||
# #(0.1, c_creates_button),
|
||||
# #(0.4, c_moves))
|
||||
## "socializing heavy builder" definition
|
||||
ACTIONS = (c_login,
|
||||
c_logout,
|
||||
(0.3, c_socialize),
|
||||
(0.1, c_looks),
|
||||
(0.1, c_help),
|
||||
(0.2, c_digs),
|
||||
(0.3, c_moves))
|
||||
Loading…
Add table
Add a link
Reference in a new issue