refactor and port dsession tests, re-introduce --dist testing along

with a simple scheduler.
This commit is contained in:
holger krekel
2010-09-28 16:32:42 +02:00
parent 3767ce3269
commit 9df55b1c60
11 changed files with 281 additions and 797 deletions

View File

@@ -1,17 +1,59 @@
import py
import sys
from xdist.slavemanage import NodeManager
from py._test import session
queue = py.builtin._tryimport('queue', 'Queue')
def dsession_main(config):
config.pluginmanager.do_configure(config)
session = DSession(config)
trdist = TerminalDistReporter(config)
config.pluginmanager.register(trdist, "terminaldistreporter")
exitcode = session.main()
config.pluginmanager.do_unconfigure(config)
return exitcode
class EachScheduling:
def __init__(self, numnodes, log=None):
self.numnodes = numnodes
self.node2collection = {}
self.node2pending = {}
if log is None:
self.log = py.log.Producer("eachsched")
else:
self.log = log.loadsched
self.collection_is_completed = False
def hasnodes(self):
return bool(self.node2pending)
def addnode(self, node):
self.node2collection[node] = None
def tests_finished(self):
if not self.collection_is_completed:
return False
for items in self.node2pending.values():
if items:
return False
return True
def addnode_collection(self, node, collection):
assert not self.collection_is_completed
assert self.node2collection[node] is None
self.node2collection[node] = list(collection)
self.node2pending[node] = []
if len(self.node2pending) >= self.numnodes:
self.collection_is_completed = True
def remove_item(self, node, item):
self.node2pending[node].remove(item)
def remove_node(self, node):
# KeyError if we didn't get an addnode() yet
pending = self.node2pending.pop(node)
if not pending:
return
crashitem = pending.pop(0)
# XXX what about the rest of pending?
return crashitem
def init_distribute(self):
assert self.collection_is_completed
for node, pending in self.node2pending.items():
node.send_runtest_all()
pending[:] = self.node2collection[node]
class LoadScheduling:
LOAD_THRESHOLD_NEWITEMS = 5
@@ -92,29 +134,20 @@ class LoadScheduling:
for node, collection in self.node2collection.items():
assert collection == col
self.pending = col
def triggertesting(self):
if not self.pending:
if not col:
return
available = []
for node, pending in self.node2pending.items():
if len(pending) < self.LOAD_THRESHOLD_NEWITEMS:
available.append((node, pending))
num_available = len(available)
if num_available:
max_one_round = num_available * self.ITEM_CHUNKSIZE -1
for i, item in enumerate(self.pending):
nodeindex = i % num_available
node, pending = available[nodeindex]
node.send_runtest(item)
self.item2nodes.setdefault(item, []).append(node)
#item.ihook.pytest_itemstart(item=item, node=node)
pending.append(item)
if i >= max_one_round:
break
del self.pending[:i+1]
if self.pending:
self.log("triggertesting remaining:", len(self.pending))
available = list(self.node2pending.items())
num_available = self.numnodes
max_one_round = num_available * self.ITEM_CHUNKSIZE -1
for i, item in enumerate(self.pending):
nodeindex = i % num_available
node, pending = available[nodeindex]
node.send_runtest(item)
self.item2nodes.setdefault(item, []).append(node)
pending.append(item)
if i >= max_one_round:
break
del self.pending[:i+1]
class Interrupted(KeyboardInterrupt):
""" signals an immediate interruption. """
@@ -138,23 +171,59 @@ class DSession:
if self.terminal:
self.terminal.write_line(line)
def pytest_gwmanage_rsyncstart(self, source, gateways):
targets = ",".join([gw.id for gw in gateways])
msg = "[%s] rsyncing: %s" %(targets, source)
self.report_line(msg)
def pytest_sessionstart(self, session, __multicall__):
#print "remaining multicall methods", __multicall__.methods
if not self.config.getvalue("verbose"):
self.report_line("instantiating gateways (use -v for details): %s" %
",".join(self.config.option.tx))
self.nodemanager = NodeManager(self.config)
self.nodemanager.setup_nodes(putevent=self.queue.put)
#def pytest_gwmanage_rsyncfinish(self, source, gateways):
# targets = ", ".join(["[%s]" % gw.id for gw in gateways])
# self.write_line("rsyncfinish: %s -> %s" %(source, targets))
def pytest_sessionfinish(self, session):
""" teardown any resources after a test run. """
self.nodemanager.teardown_nodes()
def main(self):
self.config.hook.pytest_sessionstart(session=self)
self.setup()
exitstatus = self.loop()
self.teardown()
self.config.hook.pytest_sessionfinish(session=self,
exitstatus=exitstatus,)
return exitstatus
def pytest_perform_collection(self, __multicall__):
# prohibit collection of test items in master process
__multicall__.methods[:] = []
def pytest_runtest_mainloop(self):
numnodes = len(self.nodemanager.gwmanager.specs)
dist = self.config.getvalue("dist")
if dist == "load":
self.sched = LoadScheduling(numnodes, log=self.log)
elif dist == "each":
self.sched = EachScheduling(numnodes, log=self.log)
else:
assert 0, dist
self.shouldstop = False
self.session_finished = False
while not self.session_finished:
self.loop_once()
if self.shouldstop:
raise Interrupted(str(self.shouldstop))
return True
def loop_once(self):
""" process one callback from one of the slaves. """
while 1:
try:
eventcall = self.queue.get(timeout=2.0)
break
except queue.Empty:
continue
callname, kwargs = eventcall
assert callname, kwargs
method = "slave_" + callname
call = getattr(self, method)
self.log("calling method: %s(**%s)" % (method, kwargs))
call(**kwargs)
if self.sched.tests_finished():
self.triggershutdown()
#
# callbacks for processing events from slaves
#
def slave_slaveready(self, node, slaveinfo):
node.slaveinfo = slaveinfo
@@ -192,7 +261,6 @@ class DSession:
if self.sched.collection_is_completed:
self.sched.init_distribute()
self.sched.triggertesting()
def slave_logstart(self, node, nodeid, location):
self.config.hook.pytest_runtest_logstart(
@@ -222,45 +290,6 @@ class DSession:
self.shouldstop = "stopping after %d failures" % (
self.countfailures)
def loop(self):
numnodes = len(self.nodemanager.gwmanager.specs)
self.sched = LoadScheduling(numnodes, log=self.log)
self.shouldstop = False
self.session_finished = False
exitstatus = 0
try:
while not self.session_finished:
self.loop_once()
if self.shouldstop:
raise Interrupted(str(self.shouldstop))
except KeyboardInterrupt:
excinfo = py.code.ExceptionInfo()
self.config.hook.pytest_keyboard_interrupt(excinfo=excinfo)
exitstatus = session.EXIT_INTERRUPTED
except:
self.config.pluginmanager.notify_exception()
exitstatus = session.EXIT_INTERNALERROR
#self.config.pluginmanager.unregister(loopstate)
if exitstatus == 0 and self.countfailures:
exitstatus = session.EXIT_TESTSFAILED
return exitstatus
def loop_once(self):
while 1:
try:
eventcall = self.queue.get(timeout=2.0)
break
except queue.Empty:
continue
callname, kwargs = eventcall
assert callname, kwargs
method = "slave_" + callname
call = getattr(self, method)
self.log("calling method: %s(**%s)" % (method, kwargs))
call(**kwargs)
if self.sched.tests_finished():
self.triggershutdown()
def triggershutdown(self):
self.log("triggering shutdown")
self.shuttingdown = True
@@ -277,18 +306,6 @@ class DSession:
enrich_report_with_platform_data(rep, slave)
self.config.hook.pytest_runtest_logreport(report=rep)
def setup(self):
""" setup any neccessary resources ahead of the test run. """
if not self.config.getvalue("verbose"):
self.report_line("instantiating gateways (use -v for details): %s" %
",".join(self.config.option.tx))
self.nodemanager = NodeManager(self.config)
self.nodemanager.setup_nodes(putevent=self.queue.put)
def teardown(self):
""" teardown any resources after a test run. """
self.nodemanager.teardown_nodes()
class TerminalDistReporter:
def __init__(self, config):
self.config = config
@@ -305,9 +322,9 @@ class TerminalDistReporter:
gateway.id, rinfo.platform, version, rinfo.cwd))
def pytest_testnodeready(self, node):
if self.config.getvalue("debug"):
if self.config.getvalue("verbose"):
d = node.slaveinfo
infoline = "[%s] -- Python %s" %(
infoline = "[%s] Python %s" %(
d['id'],
d['version'].replace('\n', ' -- '),)
self.write_line(infoline)
@@ -317,6 +334,15 @@ class TerminalDistReporter:
return
self.write_line("[%s] node down: %s" %(node.gateway.id, error))
#def pytest_gwmanage_rsyncstart(self, source, gateways):
# targets = ",".join([gw.id for gw in gateways])
# msg = "[%s] rsyncing: %s" %(targets, source)
# self.write_line(msg)
#def pytest_gwmanage_rsyncfinish(self, source, gateways):
# targets = ", ".join(["[%s]" % gw.id for gw in gateways])
# self.write_line("rsyncfinish: %s -> %s" %(source, targets))
def enrich_report_with_platform_data(rep, node):
rep.node = node
if hasattr(rep, 'node') and rep.longrepr:

View File

@@ -190,9 +190,16 @@ def pytest_cmdline_main(config):
from xdist.looponfail import looponfail_main
looponfail_main(config)
return 2 # looponfail only can get stop with ctrl-C anyway
elif config.getvalue("dist") != "no":
from xdist.dsession import dsession_main
return dsession_main(config)
def pytest_configure(config, __multicall__):
__multicall__.execute()
if config.getvalue("dist") != "no":
from xdist.dsession import DSession, TerminalDistReporter
session = DSession(config)
config.pluginmanager.register(session, "dsession")
trdist = TerminalDistReporter(config)
config.pluginmanager.register(trdist, "terminaldistreporter")
def check_options(config):
if config.option.numprocesses:
@@ -224,7 +231,8 @@ def forked_run_report(item):
from py._plugin.pytest_runner import runtestprotocol
EXITSTATUS_TESTEXIT = 4
import marshal
from xdist.remote import serialize_report, unserialize_report
from xdist.remote import serialize_report
from xdist.slavemanage import unserialize_report
def runforked():
try:
reports = runtestprotocol(item, log=False)
@@ -236,7 +244,7 @@ def forked_run_report(item):
result = ff.waitfinish()
if result.retval is not None:
report_dumps = marshal.loads(result.retval)
return [unserialize_report(x) for x in report_dumps]
return [unserialize_report("testreport", x) for x in report_dumps]
else:
if result.exitstatus == EXITSTATUS_TESTEXIT:
py.test.exit("forked test item %s raised Exit" %(item,))

View File

@@ -55,6 +55,9 @@ class SlaveInteractor:
for nodeid in ids:
for item in self.collection.getbyid(nodeid):
self.config.hook.pytest_runtest_protocol(item=item)
elif name == "runtests_all":
for item in self.collection.items:
self.config.hook.pytest_runtest_protocol(item=item)
elif name == "shutdown":
break
return True
@@ -65,7 +68,7 @@ class SlaveInteractor:
topdir=str(collection.topdir),
ids=ids)
#def pytest_runtest_logstart(self, nodeid, location):
#def pytest_runtest_logstart(self, nodeid, location, fspath):
# self.sendevent("logstart", nodeid=nodeid, location=location)
def pytest_runtest_logreport(self, report):

View File

@@ -233,9 +233,13 @@ class SlaveController(object):
args = self.config.args
if not spec.popen or spec.chdir:
args = make_reltoroot(self.nodemanager.roots, args)
option_dict = vars(self.config.option)
if spec.popen:
name = "popen-%s" % self.gateway.id
option_dict['basetemp'] = str(self.config.getbasetemp().join(name))
self.config.hook.pytest_configure_node(node=self)
self.channel = self.gateway.remote_exec(xdist.remote)
self.channel.send((self.slaveinput, args, vars(self.config.option)))
self.channel.send((self.slaveinput, args, option_dict))
if self.putevent:
self.channel.setcallback(self.process_from_remote,
endmarker=self.ENDMARK)
@@ -254,6 +258,9 @@ class SlaveController(object):
def send_runtest(self, nodeid):
self.sendcommand("runtests", ids=[nodeid])
def send_runtest_all(self):
self.sendcommand("runtests_all",)
def shutdown(self):
if not self._down and not self.channel.isclosed():
self.sendcommand("shutdown")

View File

@@ -1,175 +0,0 @@
"""
Manage setup, running and local representation of remote nodes/processes.
"""
import py
from py._test.session import Session
class TXNode(object):
""" Represents a Test Execution environment in the controlling process.
- sets up a slave node through an execnet gateway
- manages sending of test-items and receival of results and events
- creates events when the remote side crashes
"""
ENDMARK = -1
def __init__(self, nodemanager, gateway, config, putevent):
self.nodemanager = nodemanager
self.config = config
self.putevent = putevent
self.gateway = gateway
self.slaveinput = {}
self.channel = self.setup()
self.channel.setcallback(self.callback, endmarker=self.ENDMARK)
self._down = False
def __repr__(self):
id = self.gateway.id
status = self._down and 'true' or 'false'
return "<TXNode %r down=%s>" %(id, status)
def notify(self, eventname, *args, **kwargs):
assert not args
self.putevent((eventname, args, kwargs))
def callback(self, eventcall):
""" this gets called for each object we receive from
the other side and if the channel closes.
Note that channel callbacks run in the receiver
thread of execnet gateways - we need to
avoid raising exceptions or doing heavy work.
"""
try:
if eventcall == self.ENDMARK:
err = self.channel._getremoteerror()
if not self._down:
if not err or isinstance(err, EOFError):
err = "Not properly terminated" # lost connection?
self.notify("pytest_testnodedown", node=self, error=err)
self._down = True
return
eventname, args, kwargs = eventcall
if eventname == "slaveready":
self.notify("pytest_testnodeready", node=self)
elif eventname == "slavefinished":
self._down = True
self.slaveoutput = kwargs['slaveoutput']
error = kwargs['error']
self.notify("pytest_testnodedown", error=error, node=self)
elif eventname in ("pytest_runtest_logreport",
"pytest__teardown_final_logerror"):
kwargs['report'].node = self
self.notify(eventname, **kwargs)
else:
self.notify(eventname, **kwargs)
except KeyboardInterrupt:
# should not land in receiver-thread
raise
except:
excinfo = py.code.ExceptionInfo()
py.builtin.print_("!" * 20, excinfo)
self.config.pluginmanager.notify_exception(excinfo)
def send(self, item):
assert item is not None
self.channel.send(item)
def sendlist(self, itemlist):
self.channel.send(itemlist)
def shutdown(self, kill=False):
if kill:
self.gateway.exit()
else:
self.channel.send(None)
# configuring and setting up slave node
def setup(self):
basetemp = None
config = self.config
config.hook.pytest_configure_node(node=self)
if self.gateway.spec.popen:
popenbase = config.ensuretemp("popen")
basetemp = py.path.local.make_numbered_dir(prefix="slave-",
keep=0, rootdir=popenbase)
basetemp = str(basetemp)
return self.gateway.remote_exec(init_slave_session,
args=self.config.args,
option_dict=vars(self.config.option),
slaveinput={}, # XXX,
basetemp=basetemp,
nodeid=self.gateway.id,
)
def init_slave_session(channel, args, option_dict,
slaveinput, basetemp, nodeid):
import os, sys
#sys.path.insert(0, os.getcwd())
from xdist.txnode import SlaveSession
import py
config = py.test.config
config.option.__dict__.update(option_dict)
config._preparse(args)
config.args = args
config.slaveinput = slaveinput
config.slaveoutput = {}
if basetemp:
config.basetemp = py.path.local(basetemp)
config.nodeid = nodeid
return SlaveSession(config, channel).dist_main()
class SlaveSession:
def __init__(self, config, channel):
self.channel = channel
self.config = config
self.runner = self.config.pluginmanager.getplugin("pytest_runner")
config.pluginmanager.register(self, "slavesession")
def __repr__(self):
return "<%s channel=%s>" %(self.__class__.__name__, self.channel)
def sendevent(self, eventname, *args, **kwargs):
self.channel.send((eventname, args, kwargs))
def pytest_runtest_logreport(self, report):
self.sendevent("pytest_runtest_logreport", report=report)
def pytest__teardown_final_logerror(self, report):
self.sendevent("pytest__teardown_final_logerror", report=report)
def pytest_keyboard_interrupt(self, excinfo):
self._slaveerror = "SIGINT"
def pytest_internalerror(self, excrepr):
self._slaveerror = "internal-error"
self.sendevent("pytest_internalerror", excrepr=excrepr)
def dist_main(self):
self.sendevent("slaveready")
self.main(None)
error = getattr(self, '_slaveerror', None)
self.sendevent("slavefinished", error=error,
slaveoutput=self.config.slaveoutput)
def _mainloop(self, colitems):
while 1:
task = self.channel.receive()
if task is None:
break
if isinstance(task, list):
for item in task:
self.run_single(item=item)
else:
self.run_single(item=task)
def run_single(self, item):
call = self.runner.CallInfo(item._reraiseunpicklingproblem, when='setup')
if call.excinfo:
# likely it is not collectable here because of
# platform/import-dependency induced skips
# we fake a setup-error report with the obtained exception
# and do not care about capturing or non-runner hooks
rep = self.runner.pytest_runtest_makereport(item=item, call=call)
self.pytest_runtest_logreport(rep)
return
item.config.hook.pytest_runtest_protocol(item=item)