pytest-xdist-1.8/0000755000000000000000000000000011760166052012545 5ustar rootrootpytest-xdist-1.8/xdist/0000755000000000000000000000000011760166052013700 5ustar rootrootpytest-xdist-1.8/xdist/slavemanage.py0000644000000000000000000002604211672631270016542 0ustar rootrootimport py, pytest import sys, os import execnet import xdist.remote from _pytest import runner # XXX load dynamically class NodeManager(object): EXIT_TIMEOUT = 10 def __init__(self, config, specs=None, defaultchdir="pyexecnetcache"): self.config = config self._nodesready = py.std.threading.Event() self.trace = self.config.trace.get("nodemanager") self.group = execnet.Group() if specs is None: specs = self._getxspecs() self.specs = [] for spec in specs: if not isinstance(spec, execnet.XSpec): spec = execnet.XSpec(spec) if not spec.chdir and not spec.popen: spec.chdir = defaultchdir self.group.allocate_id(spec) self.specs.append(spec) self.roots = self._getrsyncdirs() def rsync_roots(self): """ make sure that all remote gateways have the same set of roots in their current directory. """ options = { 'ignores': self.config.getini("rsyncignore"), 'verbose': self.config.option.verbose, } if self.roots: # send each rsync root for root in self.roots: self.rsync(root, **options) def makegateways(self): assert not list(self.group) self.config.hook.pytest_xdist_setupnodes(config=self.config, specs=self.specs) for spec in self.specs: gw = self.group.makegateway(spec) self.config.hook.pytest_xdist_newgateway(gateway=gw) def setup_nodes(self, putevent): self.makegateways() self.rsync_roots() self.trace("setting up nodes") for gateway in self.group: node = SlaveController(self, gateway, self.config, putevent) gateway.node = node # to keep node alive node.setup() self.trace("started node %r" % node) def teardown_nodes(self): self.group.terminate(self.EXIT_TIMEOUT) def _getxspecs(self): xspeclist = [] for xspec in self.config.getvalue("tx"): i = xspec.find("*") try: num = int(xspec[:i]) except ValueError: xspeclist.append(xspec) else: xspeclist.extend([xspec[i+1:]] * num) if not xspeclist: raise pytest.UsageError( "MISSING test execution (tx) nodes: please specify --tx") return [execnet.XSpec(x) for x in xspeclist] def _getrsyncdirs(self): for spec in self.specs: if not spec.popen or spec.chdir: break else: return [] import pytest, _pytest pytestpath = pytest.__file__.rstrip("co") pytestdir = py.path.local(_pytest.__file__).dirpath() config = self.config candidates = [py._pydir,pytestpath,pytestdir] candidates += config.option.rsyncdir rsyncroots = config.getini("rsyncdirs") if rsyncroots: candidates.extend(rsyncroots) roots = [] for root in candidates: root = py.path.local(root).realpath() if not root.check(): raise pytest.UsageError("rsyncdir doesn't exist: %r" %(root,)) if root not in roots: roots.append(root) return roots def rsync(self, source, notify=None, verbose=False, ignores=None): """ perform rsync to all remote hosts. """ rsync = HostRSync(source, verbose=verbose, ignores=ignores) seen = py.builtin.set() gateways = [] for gateway in self.group: spec = gateway.spec if spec.popen and not spec.chdir: # XXX this assumes that sources are python-packages # and that adding the basedir does not hurt gateway.remote_exec(""" import sys ; sys.path.insert(0, %r) """ % os.path.dirname(str(source))).waitclose() continue if spec not in seen: def finished(): if notify: notify("rsyncrootready", spec, source) rsync.add_target_host(gateway, finished=finished) seen.add(spec) gateways.append(gateway) if seen: self.config.hook.pytest_xdist_rsyncstart( source=source, gateways=gateways, ) rsync.send() self.config.hook.pytest_xdist_rsyncfinish( source=source, gateways=gateways, ) class HostRSync(execnet.RSync): """ RSyncer that filters out common files """ def __init__(self, sourcedir, *args, **kwargs): self._synced = {} ignores= None if 'ignores' in kwargs: ignores = kwargs.pop('ignores') self._ignores = ignores or [] super(HostRSync, self).__init__(sourcedir=sourcedir, **kwargs) def filter(self, path): path = py.path.local(path) if not path.ext in ('.pyc', '.pyo'): if not path.basename.endswith('~'): if path.check(dotfile=0): for x in self._ignores: if path == x: break else: return True def add_target_host(self, gateway, finished=None): remotepath = os.path.basename(self._sourcedir) super(HostRSync, self).add_target(gateway, remotepath, finishedcallback=finished, delete=True,) def _report_send_file(self, gateway, modified_rel_path): if self._verbose: path = os.path.basename(self._sourcedir) + "/" + modified_rel_path remotepath = gateway.spec.chdir py.builtin.print_('%s:%s <= %s' % (gateway.spec, remotepath, path)) def make_reltoroot(roots, args): # XXX introduce/use public API for splitting py.test args splitcode = "::" l = [] for arg in args: parts = arg.split(splitcode) fspath = py.path.local(parts[0]) for root in roots: x = fspath.relto(root) if x or fspath == root: parts[0] = root.basename + "/" + x break else: raise ValueError("arg %s not relative to an rsync root" % (arg,)) l.append(splitcode.join(parts)) return l class SlaveController(object): ENDMARK = -1 def __init__(self, nodemanager, gateway, config, putevent): self.nodemanager = nodemanager self.putevent = putevent self.gateway = gateway self.config = config self.slaveinput = {'slaveid': gateway.id} self._down = False self.log = py.log.Producer("slavectl-%s" % gateway.id) if not self.config.option.debug: py.log.setconsumer(self.log._keywords, None) def __repr__(self): return "<%s %s>" %(self.__class__.__name__, self.gateway.id,) def setup(self): self.log("setting up slave session") spec = self.gateway.spec 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 basetemp = self.config._tmpdirhandler.getbasetemp() option_dict['basetemp'] = str(basetemp.join(name)) self.config.hook.pytest_configure_node(node=self) self.channel = self.gateway.remote_exec(xdist.remote) self.channel.send((self.slaveinput, args, option_dict)) if self.putevent: self.channel.setcallback(self.process_from_remote, endmarker=self.ENDMARK) def ensure_teardown(self): if hasattr(self, 'channel'): if not self.channel.isclosed(): self.log("closing", self.channel) self.channel.close() #del self.channel if hasattr(self, 'gateway'): self.log("exiting", self.gateway) self.gateway.exit() #del self.gateway 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: try: self.sendcommand("shutdown") except IOError: pass def sendcommand(self, name, **kwargs): """ send a named parametrized command to the other side. """ self.log("sending command %s(**%s)" % (name, kwargs)) self.channel.send((name, kwargs)) def notify_inproc(self, eventname, **kwargs): self.log("queuing %s(**%s)" % (eventname, kwargs)) self.putevent((eventname, kwargs)) def process_from_remote(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_inproc("errordown", node=self, error=err) self._down = True return eventname, kwargs = eventcall if eventname in ("collectionstart"): self.log("ignoring %s(%s)" %(eventname, kwargs)) elif eventname == "slaveready": self.notify_inproc(eventname, node=self, **kwargs) elif eventname == "slavefinished": self._down = True self.slaveoutput = kwargs['slaveoutput'] self.notify_inproc("slavefinished", node=self) #elif eventname == "logstart": # self.notify_inproc(eventname, node=self, **kwargs) elif eventname in ("testreport", "collectreport", "teardownreport"): rep = unserialize_report(eventname, kwargs['data']) self.notify_inproc(eventname, node=self, rep=rep) elif eventname == "collectionfinish": self.notify_inproc(eventname, node=self, ids=kwargs['ids']) else: raise ValueError("unknown event: %s" %(eventname,)) 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 unserialize_report(name, reportdict): d = reportdict if name == "testreport": return runner.TestReport(**d) elif name == "collectreport": return runner.CollectReport(**d) pytest-xdist-1.8/xdist/remote.py0000644000000000000000000001132511672631270015550 0ustar rootroot""" This module is executed in remote subprocesses and helps to control a remote testing session and relay back information. It assumes that 'py' is importable and does not have dependencies on the rest of the xdist code. This means that the xdist-plugin needs not to be installed in remote environments. """ import sys, os class SlaveInteractor: def __init__(self, config, channel): self.config = config self.slaveid = config.slaveinput.get('slaveid', "?") self.log = py.log.Producer("slave-%s" % self.slaveid) if not config.option.debug: py.log.setconsumer(self.log._keywords, None) self.channel = channel config.pluginmanager.register(self) def sendevent(self, name, **kwargs): self.log("sending", name, kwargs) self.channel.send((name, kwargs)) def pytest_internalerror(self, excrepr): for line in str(excrepr).split("\n"): self.log("IERROR> " + line) def pytest_sessionstart(self, session): self.session = session slaveinfo = getinfodict() self.sendevent("slaveready", slaveinfo=slaveinfo) def pytest_sessionfinish(self, __multicall__, exitstatus): self.config.slaveoutput['exitstatus'] = exitstatus res = __multicall__.execute() self.sendevent("slavefinished", slaveoutput=self.config.slaveoutput) return res def pytest_collection(self, session): self.sendevent("collectionstart") def pytest_runtestloop(self, session): self.log("entering main loop") torun = [] while 1: name, kwargs = self.channel.receive() self.log("received command %s(**%s)" % (name, kwargs)) if name == "runtests": ids = kwargs['ids'] for nodeid in ids: torun.append(self._id2item[nodeid]) elif name == "runtests_all": torun.extend(session.items) self.log("items to run: %s" %(len(torun))) while len(torun) >= 2: item = torun.pop(0) nextitem = torun[0] self.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem) if name == "shutdown": while torun: self.config.hook.pytest_runtest_protocol( item=torun.pop(0), nextitem=None) break return True def pytest_collection_finish(self, session): self._id2item = {} ids = [] for item in session.items: self._id2item[item.nodeid] = item ids.append(item.nodeid) self.sendevent("collectionfinish", topdir=str(session.fspath), ids=ids) #def pytest_runtest_logstart(self, nodeid, location, fspath): # self.sendevent("logstart", nodeid=nodeid, location=location) def pytest_runtest_logreport(self, report): data = serialize_report(report) self.sendevent("testreport", data=data) def pytest_collectreport(self, report): data = serialize_report(report) self.sendevent("collectreport", data=data) def serialize_report(rep): import py d = rep.__dict__.copy() if hasattr(rep.longrepr, 'toterminal'): d['longrepr'] = str(rep.longrepr) else: d['longrepr'] = rep.longrepr for name in d: if isinstance(d[name], py.path.local): d[name] = str(d[name]) elif name == "result": d[name] = None # for now return d def getinfodict(): import platform return dict( version = sys.version, version_info = tuple(sys.version_info), sysplatform = sys.platform, platform = platform.platform(), executable = sys.executable, cwd = os.getcwd(), ) def remote_initconfig(option_dict, args): from _pytest.config import Config option_dict['plugins'].append("no:terminal") config = Config.fromdictargs(option_dict, args) config.option.looponfail = False config.option.usepdb = False config.option.dist = "no" config.option.distload = False config.option.numprocesses = None config.args = args return config if __name__ == '__channelexec__': slaveinput,args,option_dict = channel.receive() importpath = os.getcwd() sys.path.insert(0, importpath) # XXX only for remote situations os.environ['PYTHONPATH'] = (importpath + os.pathsep + os.environ.get('PYTHONPATH', '')) #os.environ['PYTHONPATH'] = importpath import py config = remote_initconfig(option_dict, args) config.slaveinput = slaveinput config.slaveoutput = {} interactor = SlaveInteractor(config, channel) config.hook.pytest_cmdline_main(config=config) pytest-xdist-1.8/xdist/plugin.py0000644000000000000000000001233011672631270015550 0ustar rootrootimport sys import py, pytest def pytest_addoption(parser): group = parser.getgroup("xdist", "distributed and subprocess testing") group._addoption('-f', '--looponfail', action="store_true", dest="looponfail", default=False, help="run tests in subprocess, wait for modified files " "and re-run failing test set until all pass.") group._addoption('-n', dest="numprocesses", metavar="numprocesses", action="store", type="int", help="shortcut for '--dist=load --tx=NUM*popen'") group.addoption('--boxed', action="store_true", dest="boxed", default=False, help="box each test run in a separate process (unix)") group._addoption('--dist', metavar="distmode", action="store", choices=['load', 'each', 'no'], type="choice", dest="dist", default="no", help=("set mode for distributing tests to exec environments.\n\n" "each: send each test to each available environment.\n\n" "load: send each test to available environment.\n\n" "(default) no: run tests inprocess, don't distribute.")) group._addoption('--tx', dest="tx", action="append", default=[], metavar="xspec", help=("add a test execution environment. some examples: " "--tx popen//python=python2.5 --tx socket=192.168.1.102:8888 " "--tx ssh=user@codespeak.net//chdir=testcache")) group._addoption('-d', action="store_true", dest="distload", default=False, help="load-balance tests. shortcut for '--dist=load'") group.addoption('--rsyncdir', action="append", default=[], metavar="dir1", help="add directory for rsyncing to remote tx nodes.") parser.addini('rsyncdirs', 'list of (relative) paths to be rsynced for' ' remote distributed testing.', type="pathlist") parser.addini('rsyncignore', 'list of (relative) paths to be ignored ' 'for rsyncing.', type="pathlist") parser.addini("looponfailroots", type="pathlist", help="directories to check for changes", default=[py.path.local()]) # ------------------------------------------------------------------------- # distributed testing hooks # ------------------------------------------------------------------------- def pytest_addhooks(pluginmanager): from xdist import newhooks pluginmanager.addhooks(newhooks) # ------------------------------------------------------------------------- # distributed testing initialization # ------------------------------------------------------------------------- def pytest_cmdline_main(config): check_options(config) if config.getvalue("looponfail"): from xdist.looponfail import looponfail_main looponfail_main(config) return 2 # looponfail only can get stop with ctrl-C anyway def pytest_configure(config, __multicall__): __multicall__.execute() if config.getvalue("dist") != "no": from xdist.dsession import DSession session = DSession(config) config.pluginmanager.register(session, "dsession") def check_options(config): if config.option.numprocesses: config.option.dist = "load" config.option.tx = ['popen'] * int(config.option.numprocesses) if config.option.distload: config.option.dist = "load" val = config.getvalue if not val("collectonly"): usepdb = config.option.usepdb # a core option if val("looponfail"): if usepdb: raise pytest.UsageError("--pdb incompatible with --looponfail.") elif val("dist") != "no": if usepdb: raise pytest.UsageError("--pdb incompatible with distributing tests.") def pytest_runtest_protocol(item): if item.config.getvalue("boxed"): reports = forked_run_report(item) for rep in reports: item.ihook.pytest_runtest_logreport(report=rep) return True def forked_run_report(item): # for now, we run setup/teardown in the subprocess # XXX optionally allow sharing of setup/teardown from _pytest.runner import runtestprotocol EXITSTATUS_TESTEXIT = 4 import marshal from xdist.remote import serialize_report from xdist.slavemanage import unserialize_report def runforked(): try: reports = runtestprotocol(item, log=False) except KeyboardInterrupt: py.std.os._exit(EXITSTATUS_TESTEXIT) return marshal.dumps([serialize_report(x) for x in reports]) ff = py.process.ForkedFunc(runforked) result = ff.waitfinish() if result.retval is not None: report_dumps = marshal.loads(result.retval) 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,)) return [report_process_crash(item, result)] def report_process_crash(item, result): path, lineno = item._getfslineno() info = "%s:%s: running the test CRASHED with signal %d" %( path, lineno, result.signal) from _pytest import runner call = runner.CallInfo(lambda: 0/0, "???") call.excinfo = info rep = runner.pytest_runtest_makereport(item, call) return rep pytest-xdist-1.8/xdist/newhooks.py0000644000000000000000000000125711672631270016115 0ustar rootroot def pytest_xdist_setupnodes(config, specs): """ called before any remote node is set up. """ def pytest_xdist_newgateway(gateway): """ called on new raw gateway creation. """ def pytest_xdist_rsyncstart(source, gateways): """ called before rsyncing a directory to remote gateways takes place. """ def pytest_xdist_rsyncfinish(source, gateways): """ called after rsyncing a directory to remote gateways takes place. """ def pytest_configure_node(node): """ configure node information before it gets instantiated. """ def pytest_testnodeready(node): """ Test Node is ready to operate. """ def pytest_testnodedown(node, error): """ Test Node is down. """ pytest-xdist-1.8/xdist/looponfail.py0000644000000000000000000001712211672631270016420 0ustar rootroot""" Implement -f aka looponfailing for py.test. NOTE that we try to avoid loading and depending on application modules within the controlling process (the one that starts repeatedly test processes) otherwise changes to source code can crash the controlling process which should best never happen. """ import py, pytest import sys import execnet def looponfail_main(config): remotecontrol = RemoteControl(config) rootdirs = config.getini("looponfailroots") statrecorder = StatRecorder(rootdirs) try: while 1: remotecontrol.loop_once() if not remotecontrol.failures and remotecontrol.wasfailing: continue # the last failures passed, let's immediately rerun all repr_pytest_looponfailinfo( failreports=remotecontrol.failures, rootdirs=rootdirs) statrecorder.waitonchange(checkinterval=2.0) except KeyboardInterrupt: print() class RemoteControl(object): def __init__(self, config): self.config = config self.failures = [] def trace(self, *args): if self.config.option.debug: msg = " ".join([str(x) for x in args]) py.builtin.print_("RemoteControl:", msg) def initgateway(self): return execnet.makegateway("popen") def setup(self, out=None): if out is None: out = py.io.TerminalWriter() if hasattr(self, 'gateway'): raise ValueError("already have gateway %r" % self.gateway) self.trace("setting up slave session") self.gateway = self.initgateway() self.channel = channel = self.gateway.remote_exec(init_slave_session, args=self.config.args, option_dict=vars(self.config.option), ) remote_outchannel = channel.receive() def write(s): out._file.write(s) out._file.flush() remote_outchannel.setcallback(write) def ensure_teardown(self): if hasattr(self, 'channel'): if not self.channel.isclosed(): self.trace("closing", self.channel) self.channel.close() del self.channel if hasattr(self, 'gateway'): self.trace("exiting", self.gateway) self.gateway.exit() del self.gateway def runsession(self): try: self.trace("sending", self.failures) self.channel.send(self.failures) try: return self.channel.receive() except self.channel.RemoteError: e = sys.exc_info()[1] self.trace("ERROR", e) raise finally: self.ensure_teardown() def loop_once(self): self.setup() self.wasfailing = self.failures and len(self.failures) result = self.runsession() failures, reports, collection_failed = result if collection_failed: reports = ["Collection failed, keeping previous failure set"] else: self.failures = failures def repr_pytest_looponfailinfo(failreports, rootdirs): tr = py.io.TerminalWriter() if failreports: tr.sep("#", "LOOPONFAILING", bold=True) for report in failreports: if report: tr.line(report, red=True) tr.sep("#", "waiting for changes", bold=True) for rootdir in rootdirs: tr.line("### Watching: %s" %(rootdir,), bold=True) def init_slave_session(channel, args, option_dict): import os, sys import py outchannel = channel.gateway.newchannel() sys.stdout = sys.stderr = outchannel.makefile('w') channel.send(outchannel) # prune sys.path to not contain relative paths newpaths = [] for p in sys.path: if p: if not os.path.isabs(p): p = os.path.abspath(p) newpaths.append(p) sys.path[:] = newpaths #fullwidth, hasmarkup = channel.receive() from _pytest.config import Config config = Config.fromdictargs(option_dict, list(args)) config.args = args from xdist.looponfail import SlaveFailSession SlaveFailSession(config, channel).main() class SlaveFailSession: def __init__(self, config, channel): self.config = config self.channel = channel self.recorded_failures = [] self.collection_failed = False config.pluginmanager.register(self) config.option.looponfail = False config.option.usepdb = False def DEBUG(self, *args): if self.config.option.debug: print(" ".join(map(str, args))) def pytest_collection(self, session): self.session = session self.trails = self.current_command hook = self.session.ihook try: items = session.perform_collect(self.trails or None) except pytest.UsageError: items = session.perform_collect(None) hook.pytest_collection_modifyitems(session=session, config=session.config, items=items) hook.pytest_collection_finish(session=session) return True def pytest_runtest_logreport(self, report): if report.failed: self.recorded_failures.append(report) def pytest_collectreport(self, report): if report.failed: self.recorded_failures.append(report) self.collection_failed = True def main(self): self.DEBUG("SLAVE: received configuration, waiting for command trails") try: command = self.channel.receive() except KeyboardInterrupt: return # in the slave we can't do much about this self.DEBUG("received", command) self.current_command = command self.config.hook.pytest_cmdline_main(config=self.config) trails, failreports = [], [] for rep in self.recorded_failures: trails.append(rep.nodeid) loc = rep.longrepr loc = str(getattr(loc, 'reprcrash', loc)) failreports.append(loc) self.channel.send((trails, failreports, self.collection_failed)) class StatRecorder: def __init__(self, rootdirlist): self.rootdirlist = rootdirlist self.statcache = {} self.check() # snapshot state def fil(self, p): return p.ext in ('.py', '.txt', '.c', '.h') def rec(self, p): return p.check(dotfile=0) def waitonchange(self, checkinterval=1.0): while 1: changed = self.check() if changed: return py.std.time.sleep(checkinterval) def check(self, removepycfiles=True): changed = False statcache = self.statcache newstat = {} for rootdir in self.rootdirlist: for path in rootdir.visit(self.fil, self.rec): oldstat = statcache.pop(path, None) try: newstat[path] = curstat = path.stat() except py.error.ENOENT: if oldstat: changed = True else: if oldstat: if oldstat.mtime != curstat.mtime or \ oldstat.size != curstat.size: changed = True py.builtin.print_("# MODIFIED", path) if removepycfiles and path.ext == ".py": pycfile = path + "c" if pycfile.check(): pycfile.remove() else: changed = True if statcache: changed = True self.statcache = newstat return changed pytest-xdist-1.8/xdist/dsession.py0000644000000000000000000003257311672631270016114 0ustar rootrootimport pytest, py import sys from xdist.slavemanage import NodeManager queue = py.builtin._tryimport('queue', 'Queue') 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 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 ITEM_CHUNKSIZE = 10 def __init__(self, numnodes, log=None): self.numnodes = numnodes self.node2pending = {} self.node2collection = {} self.pending = [] if log is None: self.log = py.log.Producer("loadsched") else: self.log = log.loadsched self.collection_is_completed = False def hasnodes(self): return bool(self.node2pending) def addnode(self, node): self.node2pending[node] = [] def tests_finished(self): if not self.collection_is_completed or self.pending: 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 node in self.node2pending self.node2collection[node] = list(collection) if len(self.node2collection) >= self.numnodes: self.collection_is_completed = True def remove_item(self, node, item): if item not in self.item2nodes: raise AssertionError(item, self.item2nodes) nodes = self.item2nodes[item] if node in nodes: # the node might have gone down already nodes.remove(node) #if not nodes: # del self.item2nodes[item] pending = self.node2pending[node] pending.remove(item) # pre-load items-to-test if the node may become ready if self.pending and len(pending) < self.LOAD_THRESHOLD_NEWITEMS: item = self.pending.pop(0) pending.append(item) self.item2nodes.setdefault(item, []).append(node) node.send_runtest(item) self.log("items waiting for node: %d" %(len(self.pending))) #self.log("item2pending still executing: %s" %(self.item2nodes,)) #self.log("node2pending: %s" %(self.node2pending,)) def remove_node(self, node): pending = self.node2pending.pop(node) # KeyError if we didn't get an addnode() yet for item in pending: l = self.item2nodes[item] l.remove(node) if not l: del self.item2nodes[item] if not pending: return crashitem = pending.pop(0) self.pending.extend(pending) return crashitem def init_distribute(self): assert self.collection_is_completed assert not hasattr(self, 'item2nodes') self.item2nodes = {} # XXX allow nodes to have different collections col = list(self.node2collection.values())[0] for node, collection in self.node2collection.items(): assert collection == col self.pending = col if not col: return 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. """ class DSession: def __init__(self, config): self.config = config self.log = py.log.Producer("dsession") if not config.option.debug: py.log.setconsumer(self.log._keywords, None) self.shuttingdown = False self.countfailures = 0 self.maxfail = config.getvalue("maxfail") self.queue = queue.Queue() try: self.terminal = config.pluginmanager.getplugin("terminalreporter") except KeyError: self.terminal = None else: self.trdist = TerminalDistReporter(config) config.pluginmanager.register(self.trdist, "terminaldistreporter") def report_line(self, line): if self.terminal and self.config.option.verbose >= 0: self.terminal.write_line(line) @pytest.mark.trylast def pytest_sessionstart(self, session): self.nodemanager = NodeManager(self.config) self.nodemanager.setup_nodes(putevent=self.queue.put) def pytest_sessionfinish(self, session): """ teardown any resources after a test run. """ nm = getattr(self, 'nodemanager', None) # if not fully initialized if nm is not None: nm.teardown_nodes() def pytest_collection(self): # prohibit collection of test items in master process return True def pytest_runtestloop(self): numnodes = len(self.nodemanager.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 node.slaveinfo['id'] = node.gateway.id node.slaveinfo['spec'] = node.gateway.spec self.config.hook.pytest_testnodeready(node=node) self.sched.addnode(node) if self.shuttingdown: node.shutdown() def slave_slavefinished(self, node): self.config.hook.pytest_testnodedown(node=node, error=None) if node.slaveoutput['exitstatus'] == 2: # keyboard-interrupt self.shouldstop = "%s received keyboard-interrupt" % (node,) self.slave_errordown(node, "keyboard-interrupt") return crashitem = self.sched.remove_node(node) #assert not crashitem, (crashitem, node) if self.shuttingdown and not self.sched.hasnodes(): self.session_finished = True def slave_errordown(self, node, error): self.config.hook.pytest_testnodedown(node=node, error=error) try: crashitem = self.sched.remove_node(node) except KeyError: pass else: if crashitem: self.handle_crashitem(crashitem, node) #self.report_line("item crashed on node: %s" % crashitem) if not self.sched.hasnodes(): self.session_finished = True def slave_collectionfinish(self, node, ids): self.sched.addnode_collection(node, ids) if self.terminal: self.trdist.setstatus(node.gateway.spec, "[%d]" %(len(ids))) if self.sched.collection_is_completed: if self.terminal: self.trdist.ensure_show_status() self.terminal.write_line("") self.terminal.write_line("scheduling tests via %s" %( self.sched.__class__.__name__)) self.sched.init_distribute() def slave_logstart(self, node, nodeid, location): self.config.hook.pytest_runtest_logstart( nodeid=nodeid, location=location) def slave_testreport(self, node, rep): if not (rep.passed and rep.when != "call"): if rep.when in ("setup", "call"): self.sched.remove_item(node, rep.nodeid) #self.report_line("testreport %s: %s" %(rep.id, rep.status)) rep.node = node self.config.hook.pytest_runtest_logreport(report=rep) self._handlefailures(rep) def slave_collectreport(self, node, rep): #self.report_line("collectreport %s: %s" %(rep.id, rep.status)) #rep.node = node self._handlefailures(rep) def _handlefailures(self, rep): if rep.failed: self.countfailures += 1 if self.maxfail and self.countfailures >= self.maxfail: self.shouldstop = "stopping after %d failures" % ( self.countfailures) def triggershutdown(self): self.log("triggering shutdown") self.shuttingdown = True for node in self.sched.node2pending: node.shutdown() def handle_crashitem(self, nodeid, slave): # XXX get more reporting info by recording pytest_runtest_logstart? runner = self.config.pluginmanager.getplugin("runner") fspath = nodeid.split("::")[0] msg = "Slave %r crashed while running %r" %(slave.gateway.id, nodeid) rep = runner.TestReport(nodeid, (fspath, None, fspath), (), "failed", msg, "???") rep.node = slave self.config.hook.pytest_runtest_logreport(report=rep) class TerminalDistReporter: def __init__(self, config): self.config = config self.tr = config.pluginmanager.getplugin("terminalreporter") self._status = {} self._lastlen = 0 def write_line(self, msg): self.tr.write_line(msg) def ensure_show_status(self): if not self.tr.hasmarkup: self.write_line(self.getstatus()) def setstatus(self, spec, status, show=True): self._status[spec.id] = status if show and self.tr.hasmarkup: self.rewrite(self.getstatus()) def getstatus(self): parts = ["%s %s" %(spec.id, self._status[spec.id]) for spec in self._specs] return " / ".join(parts) def rewrite(self, line, newline=False): pline = line + " " * max(self._lastlen-len(line), 0) if newline: self._lastlen = 0 pline += "\n" else: self._lastlen = len(line) self.tr.rewrite(pline, bold=True) def pytest_xdist_setupnodes(self, specs): self._specs = specs for spec in specs: self.setstatus(spec, "I", show=False) self.setstatus(spec, "I", show=True) self.ensure_show_status() def pytest_xdist_newgateway(self, gateway): if self.config.option.verbose > 0: rinfo = gateway._rinfo() version = "%s.%s.%s" % rinfo.version_info[:3] self.rewrite("[%s] %s Python %s cwd: %s" % ( gateway.id, rinfo.platform, version, rinfo.cwd), newline=True) self.setstatus(gateway.spec, "C") def pytest_testnodeready(self, node): if self.config.option.verbose > 0: d = node.slaveinfo infoline = "[%s] Python %s" %( d['id'], d['version'].replace('\n', ' -- '),) self.rewrite(infoline, newline=True) self.setstatus(node.gateway.spec, "ok") def pytest_testnodedown(self, node, error): if not error: return self.write_line("[%s] node down: %s" %(node.gateway.id, error)) #def pytest_xdist_rsyncstart(self, source, gateways): # targets = ",".join([gw.id for gw in gateways]) # msg = "[%s] rsyncing: %s" %(targets, source) # self.write_line(msg) #def pytest_xdist_rsyncfinish(self, source, gateways): # targets = ", ".join(["[%s]" % gw.id for gw in gateways]) # self.write_line("rsyncfinish: %s -> %s" %(source, targets)) pytest-xdist-1.8/xdist/__init__.py0000644000000000000000000000002611672631270016010 0ustar rootroot# __version__ = '1.8' pytest-xdist-1.8/testing/0000755000000000000000000000000011760166052014222 5ustar rootrootpytest-xdist-1.8/testing/test_slavemanage.py0000644000000000000000000002140311672631270020117 0ustar rootrootimport py import os import execnet from xdist.slavemanage import HostRSync, NodeManager pytest_plugins = "pytester", def pytest_funcarg__hookrecorder(request): _pytest = request.getfuncargvalue('_pytest') config = request.getfuncargvalue('config') return _pytest.gethookrecorder(config.hook) def pytest_funcarg__config(request): testdir = request.getfuncargvalue("testdir") config = testdir.parseconfig() return config class pytest_funcarg__mysetup: def __init__(self, request): temp = request.getfuncargvalue("tmpdir") self.source = temp.mkdir("source") self.dest = temp.mkdir("dest") request.getfuncargvalue("_pytest") class TestNodeManagerPopen: def test_popen_no_default_chdir(self, config): gm = NodeManager(config, ["popen"]) assert gm.specs[0].chdir is None def test_default_chdir(self, config): l = ["ssh=noco", "socket=xyz"] for spec in NodeManager(config, l).specs: assert spec.chdir == "pyexecnetcache" for spec in NodeManager(config, l, defaultchdir="abc").specs: assert spec.chdir == "abc" def test_popen_makegateway_events(self, config, hookrecorder, _pytest): hm = NodeManager(config, ["popen"] * 2) hm.makegateways() call = hookrecorder.popcall("pytest_xdist_setupnodes") assert len(call.specs) == 2 call = hookrecorder.popcall("pytest_xdist_newgateway") assert call.gateway.spec == execnet.XSpec("popen") assert call.gateway.id == "gw0" call = hookrecorder.popcall("pytest_xdist_newgateway") assert call.gateway.id == "gw1" assert len(hm.group) == 2 hm.teardown_nodes() assert not len(hm.group) def test_popens_rsync(self, config, mysetup): source = mysetup.source hm = NodeManager(config, ["popen"] * 2) hm.makegateways() assert len(hm.group) == 2 for gw in hm.group: class pseudoexec: args = [] def __init__(self, *args): self.args.extend(args) def waitclose(self): pass gw.remote_exec = pseudoexec l = [] hm.rsync(source, notify=lambda *args: l.append(args)) assert not l hm.teardown_nodes() assert not len(hm.group) assert "sys.path.insert" in gw.remote_exec.args[0] def test_rsync_popen_with_path(self, config, mysetup): source, dest = mysetup.source, mysetup.dest hm = NodeManager(config, ["popen//chdir=%s" %dest] * 1) hm.makegateways() source.ensure("dir1", "dir2", "hello") l = [] hm.rsync(source, notify=lambda *args: l.append(args)) assert len(l) == 1 assert l[0] == ("rsyncrootready", hm.group['gw0'].spec, source) hm.teardown_nodes() dest = dest.join(source.basename) assert dest.join("dir1").check() assert dest.join("dir1", "dir2").check() assert dest.join("dir1", "dir2", 'hello').check() def test_rsync_same_popen_twice(self, config, mysetup, hookrecorder): source, dest = mysetup.source, mysetup.dest hm = NodeManager(config, ["popen//chdir=%s" %dest] * 2) hm.makegateways() source.ensure("dir1", "dir2", "hello") hm.rsync(source) call = hookrecorder.popcall("pytest_xdist_rsyncstart") assert call.source == source assert len(call.gateways) == 1 assert call.gateways[0] in hm.group call = hookrecorder.popcall("pytest_xdist_rsyncfinish") class TestHRSync: class pytest_funcarg__mysetup: def __init__(self, request): tmp = request.getfuncargvalue('tmpdir') self.source = tmp.mkdir("source") self.dest = tmp.mkdir("dest") def test_hrsync_filter(self, mysetup): source, dest = mysetup.source, mysetup.dest source.ensure("dir", "file.txt") source.ensure(".svn", "entries") source.ensure(".somedotfile", "moreentries") source.ensure("somedir", "editfile~") syncer = HostRSync(source) l = list(source.visit(rec=syncer.filter, fil=syncer.filter)) assert len(l) == 3 basenames = [x.basename for x in l] assert 'dir' in basenames assert 'file.txt' in basenames assert 'somedir' in basenames def test_hrsync_one_host(self, mysetup): source, dest = mysetup.source, mysetup.dest gw = execnet.makegateway("popen//chdir=%s" % dest) finished = [] rsync = HostRSync(source) rsync.add_target_host(gw, finished=lambda: finished.append(1)) source.join("hello.py").write("world") rsync.send() gw.exit() assert dest.join(source.basename, "hello.py").check() assert len(finished) == 1 class TestNodeManager: @py.test.mark.xfail def test_rsync_roots_no_roots(self, testdir, mysetup): mysetup.source.ensure("dir1", "file1").write("hello") config = testdir.parseconfig(source) nodemanager = NodeManager(config, ["popen//chdir=%s" % mysetup.dest]) #assert nodemanager.config.topdir == source == config.topdir nodemanager.makegateways() nodemanager.rsync_roots() p, = nodemanager.gwmanager.multi_exec( "import os ; channel.send(os.getcwd())").receive_each() p = py.path.local(p) py.builtin.print_("remote curdir", p) assert p == mysetup.dest.join(config.topdir.basename) assert p.join("dir1").check() assert p.join("dir1", "file1").check() def test_popen_rsync_subdir(self, testdir, mysetup): source, dest = mysetup.source, mysetup.dest dir1 = mysetup.source.mkdir("dir1") dir2 = dir1.mkdir("dir2") dir2.ensure("hello") for rsyncroot in (dir1, source): dest.remove() nodemanager = NodeManager(testdir.parseconfig( "--tx", "popen//chdir=%s" % dest, "--rsyncdir", rsyncroot, source, )) nodemanager.makegateways() nodemanager.rsync_roots() if rsyncroot == source: dest = dest.join("source") assert dest.join("dir1").check() assert dest.join("dir1", "dir2").check() assert dest.join("dir1", "dir2", 'hello').check() nodemanager.teardown_nodes() def test_init_rsync_roots(self, testdir, mysetup): source, dest = mysetup.source, mysetup.dest dir2 = source.ensure("dir1", "dir2", dir=1) source.ensure("dir1", "somefile", dir=1) dir2.ensure("hello") source.ensure("bogusdir", "file") source.join("tox.ini").write(py.std.textwrap.dedent(""" [pytest] rsyncdirs=dir1/dir2 """)) config = testdir.parseconfig(source) nodemanager = NodeManager(config, ["popen//chdir=%s" % dest]) nodemanager.makegateways() nodemanager.rsync_roots() assert dest.join("dir2").check() assert not dest.join("dir1").check() assert not dest.join("bogus").check() def test_rsyncignore(self, testdir, mysetup): source, dest = mysetup.source, mysetup.dest dir2 = source.ensure("dir1", "dir2", dir=1) dir5 = source.ensure("dir5", "dir6", "bogus") dirf = source.ensure("dir5", "file") dir2.ensure("hello") source.join("tox.ini").write(py.std.textwrap.dedent(""" [pytest] rsyncdirs = dir1 dir5 rsyncignore = dir1/dir2 dir5/dir6 """)) config = testdir.parseconfig(source) nodemanager = NodeManager(config, ["popen//chdir=%s" % dest]) nodemanager.makegateways() nodemanager.rsync_roots() assert dest.join("dir1").check() assert not dest.join("dir1", "dir2").check() assert dest.join("dir5","file").check() assert not dest.join("dir6").check() def test_optimise_popen(self, testdir, mysetup): source, dest = mysetup.source, mysetup.dest specs = ["popen"] * 3 source.join("conftest.py").write("rsyncdirs = ['a']") source.ensure('a', dir=1) config = testdir.parseconfig(source) nodemanager = NodeManager(config, specs) nodemanager.makegateways() nodemanager.rsync_roots() for gwspec in nodemanager.specs: assert gwspec._samefilesystem() assert not gwspec.chdir def test_ssh_setup_nodes(self, specssh, testdir): testdir.makepyfile(__init__="", test_x=""" def test_one(): pass """) reprec = testdir.inline_run("-d", "--rsyncdir=%s" % testdir.tmpdir, "--tx", specssh, testdir.tmpdir) rep, = reprec.getreports("pytest_runtest_logreport") assert rep.passed pytest-xdist-1.8/testing/test_remote.py0000644000000000000000000002055511672631270017136 0ustar rootrootimport py from xdist.slavemanage import SlaveController, unserialize_report from xdist.remote import serialize_report import execnet queue = py.builtin._tryimport("queue", "Queue") from py.builtin import print_ import marshal WAIT_TIMEOUT = 10.0 def check_marshallable(d): try: marshal.dumps(d) except ValueError: py.std.pprint.pprint(d) raise ValueError("not marshallable") class EventCall: def __init__(self, eventcall): self.name, self.kwargs = eventcall def __str__(self): return "" %(self.name, self.kwargs) class SlaveSetup: use_callback = False def __init__(self, request): self.testdir = testdir = request.getfuncargvalue("testdir") self.request = request self.events = queue.Queue() def setup(self, ): self.testdir.chdir() #import os ; os.environ['EXECNET_DEBUG'] = "2" self.gateway = execnet.makegateway() self.config = config = self.testdir.parseconfigure() putevent = self.use_callback and self.events.put or None self.slp = SlaveController(None, self.gateway, config, putevent) self.request.addfinalizer(self.slp.ensure_teardown) self.slp.setup() def popevent(self, name=None): while 1: if self.use_callback: data = self.events.get(timeout=WAIT_TIMEOUT) else: data = self.slp.channel.receive(timeout=WAIT_TIMEOUT) ev = EventCall(data) if name is None or ev.name == name: return ev print("skipping %s" % (ev,)) def sendcommand(self, name, **kwargs): self.slp.sendcommand(name, **kwargs) def pytest_funcarg__slave(request): return SlaveSetup(request) def test_remoteinitconfig(testdir): from xdist.remote import remote_initconfig config1 = testdir.parseconfig() config2 = remote_initconfig(config1.option.__dict__, config1.args) assert config2.option.__dict__ == config1.option.__dict__ assert config2.pluginmanager.getplugin("terminal") in (-1, None) class TestReportSerialization: def test_itemreport_outcomes(self, testdir): reprec = testdir.inline_runsource(""" import py def test_pass(): pass def test_fail(): 0/0 @py.test.mark.skipif("True") def test_skip(): pass def test_skip_imperative(): py.test.skip("hello") @py.test.mark.xfail("True") def test_xfail(): 0/0 def test_xfail_imperative(): py.test.xfail("hello") """) reports = reprec.getreports("pytest_runtest_logreport") assert len(reports) == 17 # with setup/teardown "passed" reports for rep in reports: d = serialize_report(rep) check_marshallable(d) newrep = unserialize_report("testreport", d) assert newrep.passed == rep.passed assert newrep.failed == rep.failed assert newrep.skipped == rep.skipped if newrep.skipped and 'xfail' not in newrep.keywords: assert len(newrep.longrepr) == 3 assert newrep.outcome == rep.outcome assert newrep.when == rep.when assert newrep.keywords == rep.keywords if rep.failed: assert newrep.longrepr == str(rep.longrepr) def test_collectreport_passed(self, testdir): reprec = testdir.inline_runsource("def test_func(): pass") reports = reprec.getreports("pytest_collectreport") for rep in reports: d = serialize_report(rep) check_marshallable(d) newrep = unserialize_report("collectreport", d) assert newrep.passed == rep.passed assert newrep.failed == rep.failed assert newrep.skipped == rep.skipped def test_collectreport_fail(self, testdir): reprec = testdir.inline_runsource("qwe abc") reports = reprec.getreports("pytest_collectreport") assert reports for rep in reports: d = serialize_report(rep) check_marshallable(d) newrep = unserialize_report("collectreport", d) assert newrep.passed == rep.passed assert newrep.failed == rep.failed assert newrep.skipped == rep.skipped if rep.failed: assert newrep.longrepr == str(rep.longrepr) class TestSlaveInteractor: def test_basic_collect_and_runtests(self, slave): p = slave.testdir.makepyfile(""" def test_func(): pass """) slave.setup() ev = slave.popevent() assert ev.name == "slaveready" ev = slave.popevent() assert ev.name == "collectionstart" assert not ev.kwargs ev = slave.popevent("collectionfinish") assert ev.kwargs['topdir'] == slave.testdir.tmpdir ids = ev.kwargs['ids'] assert len(ids) == 1 slave.sendcommand("runtests", ids=ids) slave.sendcommand("shutdown") ev = slave.popevent("testreport") # setup ev = slave.popevent("testreport") assert ev.name == "testreport" rep = unserialize_report(ev.name, ev.kwargs['data']) assert rep.nodeid.endswith("::test_func") assert rep.passed assert rep.when == "call" ev = slave.popevent("slavefinished") assert 'slaveoutput' in ev.kwargs def test_remote_collect_skip(self, slave): p = slave.testdir.makepyfile(""" import py py.test.skip("hello") """) slave.setup() ev = slave.popevent("collectionstart") assert not ev.kwargs ev = slave.popevent() assert ev.name == "collectreport" ev = slave.popevent() assert ev.name == "collectreport" rep = unserialize_report(ev.name, ev.kwargs['data']) assert rep.skipped ev = slave.popevent("collectionfinish") assert not ev.kwargs['ids'] def test_remote_collect_fail(self, slave): p = slave.testdir.makepyfile("""aasd qwe""") slave.setup() ev = slave.popevent("collectionstart") assert not ev.kwargs ev = slave.popevent() assert ev.name == "collectreport" ev = slave.popevent() assert ev.name == "collectreport" rep = unserialize_report(ev.name, ev.kwargs['data']) assert rep.failed ev = slave.popevent("collectionfinish") assert not ev.kwargs['ids'] def test_runtests_all(self, slave): p = slave.testdir.makepyfile(""" def test_func(): pass def test_func2(): pass """) slave.setup() ev = slave.popevent() assert ev.name == "slaveready" ev = slave.popevent() assert ev.name == "collectionstart" assert not ev.kwargs ev = slave.popevent("collectionfinish") ids = ev.kwargs['ids'] assert len(ids) == 2 slave.sendcommand("runtests_all", ) slave.sendcommand("shutdown", ) for func in "::test_func", "::test_func2": for i in range(3): # setup/call/teardown ev = slave.popevent("testreport") assert ev.name == "testreport" rep = unserialize_report(ev.name, ev.kwargs['data']) assert rep.nodeid.endswith(func) ev = slave.popevent("slavefinished") assert 'slaveoutput' in ev.kwargs def test_happy_run_events_converted(self, testdir, slave): py.test.xfail("implement a simple test for event production") assert not slave.use_callback p = slave.testdir.makepyfile(""" def test_func(): pass """) slave.setup() hookrec = testdir.getreportrecorder(slave.config) for data in slave.slp.channel: slave.slp.process_from_remote(data) slave.slp.process_from_remote(slave.slp.ENDMARK) py.std.pprint.pprint(hookrec.hookrecorder.calls) hookrec.hookrecorder.contains([ ("pytest_collectstart", "collector.fspath == aaa"), ("pytest_pycollect_makeitem", "name == 'test_func'"), ("pytest_collectreport", "report.collector.fspath == aaa"), ("pytest_collectstart", "collector.fspath == bbb"), ("pytest_pycollect_makeitem", "name == 'test_func'"), ("pytest_collectreport", "report.collector.fspath == bbb"), ]) pytest-xdist-1.8/testing/test_plugin.py0000644000000000000000000000446411672631270017142 0ustar rootrootimport py import execnet from xdist.slavemanage import NodeManager def test_dist_incompatibility_messages(testdir): result = testdir.runpytest("--pdb", "--looponfail") assert result.ret != 0 result = testdir.runpytest("--pdb", "-n", "3") assert result.ret != 0 assert "incompatible" in result.stderr.str() result = testdir.runpytest("--pdb", "-d", "--tx", "popen") assert result.ret != 0 assert "incompatible" in result.stderr.str() def test_dist_options(testdir): from xdist.plugin import check_options config = testdir.parseconfigure("-n 2") check_options(config) assert config.option.dist == "load" assert config.option.tx == ['popen'] * 2 config = testdir.parseconfigure("-d") check_options(config) assert config.option.dist == "load" class TestDistOptions: def test_getxspecs(self, testdir): config = testdir.parseconfigure("--tx=popen", "--tx", "ssh=xyz") nodemanager = NodeManager(config) xspecs = nodemanager._getxspecs() assert len(xspecs) == 2 print(xspecs) assert xspecs[0].popen assert xspecs[1].ssh == "xyz" def test_xspecs_multiplied(self, testdir): config = testdir.parseconfigure("--tx=3*popen",) xspecs = NodeManager(config)._getxspecs() assert len(xspecs) == 3 assert xspecs[1].popen def test_getrsyncdirs(self, testdir): config = testdir.parseconfigure('--rsyncdir=' + str(testdir.tmpdir)) nm = NodeManager(config, specs=[execnet.XSpec("popen")]) assert not nm._getrsyncdirs() nm = NodeManager(config, specs=[execnet.XSpec("popen//chdir=qwe")]) assert nm.roots assert testdir.tmpdir in nm.roots def test_getrsyncdirs_with_conftest(self, testdir): p = py.path.local() for bn in 'x y z'.split(): p.mkdir(bn) testdir.makeini(""" [pytest] rsyncdirs= x """) config = testdir.parseconfigure( testdir.tmpdir, '--rsyncdir=y', '--rsyncdir=z') nm = NodeManager(config, specs=[execnet.XSpec("popen//chdir=xyz")]) roots = nm._getrsyncdirs() #assert len(roots) == 3 + 1 # pylib assert py.path.local('y') in roots assert py.path.local('z') in roots assert testdir.tmpdir.join('x') in roots pytest-xdist-1.8/testing/test_looponfail.py0000644000000000000000000001652511672631270020007 0ustar rootrootimport py from xdist.looponfail import RemoteControl from xdist.looponfail import StatRecorder class TestStatRecorder: def test_filechange(self, tmpdir): tmp = tmpdir hello = tmp.ensure("hello.py") sd = StatRecorder([tmp]) changed = sd.check() assert not changed hello.write("world") changed = sd.check() assert changed p = tmp.ensure("new.py") changed = sd.check() assert changed p.remove() changed = sd.check() assert changed tmp.join("a", "b", "c.py").ensure() changed = sd.check() assert changed tmp.join("a", "c.txt").ensure() changed = sd.check() assert changed changed = sd.check() assert not changed tmp.join("a").remove() changed = sd.check() assert changed def test_filechange_deletion_race(self, tmpdir, monkeypatch): tmp = tmpdir sd = StatRecorder([tmp]) changed = sd.check() assert not changed p = tmp.ensure("new.py") changed = sd.check() assert changed p.remove() # make check()'s visit() call return our just removed # path as if we were in a race condition monkeypatch.setattr(tmp, 'visit', lambda *args: [p]) changed = sd.check() assert changed def test_pycremoval(self, tmpdir): tmp = tmpdir hello = tmp.ensure("hello.py") sd = StatRecorder([tmp]) changed = sd.check() assert not changed pycfile = hello + "c" pycfile.ensure() changed = sd.check() assert not changed hello.write("world") changed = sd.check() assert not pycfile.check() def test_waitonchange(self, tmpdir, monkeypatch): tmp = tmpdir sd = StatRecorder([tmp]) l = [True, False] monkeypatch.setattr(StatRecorder, 'check', lambda self: l.pop()) sd.waitonchange(checkinterval=0.2) assert not l class TestRemoteControl: def test_nofailures(self, testdir): item = testdir.getitem("def test_func(): pass\n") control = RemoteControl(item.config) control.setup() topdir, failures = control.runsession()[:2] assert not failures def test_failures_somewhere(self, testdir): item = testdir.getitem("def test_func():\n assert 0\n") control = RemoteControl(item.config) control.setup() failures = control.runsession() assert failures control.setup() item.fspath.write("def test_func():\n assert 1\n") removepyc(item.fspath) topdir, failures = control.runsession()[:2] assert not failures def test_failure_change(self, testdir): modcol = testdir.getitem(""" def test_func(): assert 0 """) control = RemoteControl(modcol.config) control.loop_once() assert control.failures modcol.fspath.write(py.code.Source(""" def test_func(): assert 1 def test_new(): assert 0 """)) removepyc(modcol.fspath) control.loop_once() assert not control.failures control.loop_once() assert control.failures assert str(control.failures).find("test_new") != -1 def test_failure_subdir_no_init(self, testdir): modcol = testdir.getitem(""" def test_func(): assert 0 """) parent = modcol.fspath.dirpath().dirpath() parent.chdir() modcol.config.args = [py.path.local(x).relto(parent) for x in modcol.config.args] control = RemoteControl(modcol.config) control.loop_once() assert control.failures control.loop_once() assert control.failures class TestLooponFailing: def test_looponfail_from_fail_to_ok(self, testdir): modcol = testdir.getmodulecol(""" def test_one(): x = 0 assert x == 1 def test_two(): assert 1 """) remotecontrol = RemoteControl(modcol.config) remotecontrol.loop_once() assert len(remotecontrol.failures) == 1 modcol.fspath.write(py.code.Source(""" def test_one(): assert 1 def test_two(): assert 1 """)) removepyc(modcol.fspath) remotecontrol.loop_once() assert not remotecontrol.failures def test_looponfail_from_one_to_two_tests(self, testdir): modcol = testdir.getmodulecol(""" def test_one(): assert 0 """) remotecontrol = RemoteControl(modcol.config) remotecontrol.loop_once() assert len(remotecontrol.failures) == 1 assert 'test_one' in remotecontrol.failures[0] modcol.fspath.write(py.code.Source(""" def test_one(): assert 1 # passes now def test_two(): assert 0 # new and fails """)) removepyc(modcol.fspath) remotecontrol.loop_once() assert len(remotecontrol.failures) == 0 remotecontrol.loop_once() assert len(remotecontrol.failures) == 1 assert 'test_one' not in remotecontrol.failures[0] assert 'test_two' in remotecontrol.failures[0] def test_looponfail_removed_test(self, testdir): modcol = testdir.getmodulecol(""" def test_one(): assert 0 def test_two(): assert 0 """) remotecontrol = RemoteControl(modcol.config) remotecontrol.loop_once() assert len(remotecontrol.failures) == 2 modcol.fspath.write(py.code.Source(""" def test_xxx(): # renamed test assert 0 def test_two(): assert 1 # pass now """)) removepyc(modcol.fspath) remotecontrol.loop_once() assert len(remotecontrol.failures) == 0 remotecontrol.loop_once() assert len(remotecontrol.failures) == 1 class TestFunctional: def test_fail_to_ok(self, testdir): p = testdir.makepyfile(""" def test_one(): x = 0 assert x == 1 """) #p = testdir.mkdir("sub").join(p1.basename) #p1.move(p) child = testdir.spawn_pytest("-f %s --traceconfig" % p) child.expect("def test_one") child.expect("x == 1") child.expect("1 failed") child.expect("### LOOPONFAILING ####") child.expect("waiting for changes") p.write(py.code.Source(""" def test_one(): x = 1 assert x == 1 """)) child.expect(".*1 passed.*") child.kill(15) def test_xfail_passes(self, testdir): p = testdir.makepyfile(""" import py @py.test.mark.xfail def test_one(): pass """) child = testdir.spawn_pytest("-f %s" % p) child.expect("1 xpass") child.expect("### LOOPONFAILING ####") child.expect("waiting for changes") child.kill(15) def removepyc(path): # XXX damn those pyc files pyc = path + "c" if pyc.check(): pyc.remove() c = path.dirpath("__pycache__") if c.check(): c.remove() pytest-xdist-1.8/testing/test_dsession.py0000644000000000000000000001300711672631270017464 0ustar rootrootfrom xdist.dsession import DSession, LoadScheduling, EachScheduling from _pytest import main as outcome import py import execnet XSpec = execnet.XSpec def run(item, node, excinfo=None): runner = item.config.pluginmanager.getplugin("runner") rep = runner.ItemTestReport(item=item, excinfo=excinfo, when="call") rep.node = node return rep class MockNode: def __init__(self): self.sent = [] def send_runtest(self, nodeid): self.sent.append(nodeid) def send_runtest_all(self): self.sent.append("ALL") def sendlist(self, items): self.sent.extend(items) def shutdown(self): self._shutdown=True def dumpqueue(queue): while queue.qsize(): print(queue.get()) class TestEachScheduling: def test_schedule_load_simple(self): node1 = MockNode() node2 = MockNode() sched = EachScheduling(2) sched.addnode(node1) sched.addnode(node2) collection = ["a.py::test_1", ] assert not sched.collection_is_completed sched.addnode_collection(node1, collection) assert not sched.collection_is_completed sched.addnode_collection(node2, collection) assert sched.collection_is_completed assert sched.node2collection[node1] == collection assert sched.node2collection[node2] == collection sched.init_distribute() assert sched.tests_finished() assert node1.sent == ['ALL'] assert node2.sent == ['ALL'] sched.remove_item(node1, collection[0]) assert sched.tests_finished() sched.remove_item(node2, collection[0]) assert sched.tests_finished() def test_schedule_remove_node(self): node1 = MockNode() sched = EachScheduling(1) sched.addnode(node1) collection = ["a.py::test_1", ] assert not sched.collection_is_completed sched.addnode_collection(node1, collection) assert sched.collection_is_completed assert sched.node2collection[node1] == collection sched.init_distribute() assert sched.tests_finished() crashitem = sched.remove_node(node1) assert crashitem assert sched.tests_finished() assert not sched.hasnodes() class TestLoadScheduling: def test_schedule_load_simple(self): node1 = MockNode() node2 = MockNode() sched = LoadScheduling(2) sched.addnode(node1) sched.addnode(node2) collection = ["a.py::test_1", "a.py::test_2"] assert not sched.collection_is_completed sched.addnode_collection(node1, collection) assert not sched.collection_is_completed sched.addnode_collection(node2, collection) assert sched.collection_is_completed assert sched.node2collection[node1] == collection assert sched.node2collection[node2] == collection sched.init_distribute() assert sched.tests_finished() assert len(node1.sent) == 1 assert len(node2.sent) == 1 x = sorted(node1.sent + node2.sent) assert x == collection sched.remove_item(node1, node1.sent[0]) sched.remove_item(node2, node2.sent[0]) assert sched.tests_finished() assert not sched.pending def test_init_distribute_chunksize(self): sched = LoadScheduling(2) node1 = MockNode() node2 = MockNode() sched.addnode(node1) sched.addnode(node2) sched.ITEM_CHUNKSIZE = 2 col = ["xyz"] * (2*sched.ITEM_CHUNKSIZE +1) sched.addnode_collection(node1, col) sched.addnode_collection(node2, col) sched.init_distribute() #assert not sched.tests_finished() sent1 = node1.sent sent2 = node2.sent chunkitems = col[:sched.ITEM_CHUNKSIZE] assert sent1 == chunkitems assert sent2 == chunkitems assert sched.node2pending[node1] == sent1 assert sched.node2pending[node2] == sent2 assert len(sched.pending) == 1 for node in (node1, node2): for i in range(sched.ITEM_CHUNKSIZE): sched.remove_item(node, "xyz") assert not sched.pending def test_add_remove_node(self): node = MockNode() sched = LoadScheduling(1) sched.addnode(node) collection = ["test_file.py::test_func"] sched.addnode_collection(node, collection) assert sched.collection_is_completed sched.init_distribute() assert not sched.pending crashitem = sched.remove_node(node) assert crashitem == collection[0] class TestDistReporter: @py.test.mark.xfail def test_rsync_printing(self, testdir, linecomp): config = testdir.parseconfig() from _pytest.pytest_terminal import TerminalReporter rep = TerminalReporter(config, file=linecomp.stringio) config.pluginmanager.register(rep, "terminalreporter") dsession = DSession(config) class gw1: id = "X1" spec = execnet.XSpec("popen") class gw2: id = "X2" spec = execnet.XSpec("popen") #class rinfo: # version_info = (2, 5, 1, 'final', 0) # executable = "hello" # platform = "xyz" # cwd = "qwe" #dsession.pytest_xdist_newgateway(gw1, rinfo) #linecomp.assert_contains_lines([ # "*X1*popen*xyz*2.5*" #]) dsession.pytest_xdist_rsyncstart(source="hello", gateways=[gw1, gw2]) linecomp.assert_contains_lines([ "[X1,X2] rsyncing: hello", ]) pytest-xdist-1.8/testing/test_boxed.py0000644000000000000000000000150411672631266016742 0ustar rootrootimport py @py.test.mark.skipif("not hasattr(os, 'fork')") def test_functional_boxed(testdir): p1 = testdir.makepyfile(""" import os def test_function(): os.kill(os.getpid(), 15) """) result = testdir.runpytest(p1, "--boxed") result.stdout.fnmatch_lines([ "*CRASHED*", "*1 failed*" ]) class TestOptionEffects: def test_boxed_option_default(self, testdir): tmpdir = testdir.tmpdir.ensure("subdir", dir=1) config = testdir.parseconfig() assert not config.option.boxed py.test.importorskip("execnet") config = testdir.parseconfig('-d', tmpdir) assert not config.option.boxed def test_is_not_boxed_by_default(self, testdir): config = testdir.parseconfig(testdir.tmpdir) assert not config.option.boxed pytest-xdist-1.8/testing/conftest.py0000644000000000000000000000211611672631266016427 0ustar rootrootimport py import execnet pytest_plugins = "pytester" #rsyncdirs = ['.', '../xdist', py.path.local(execnet.__file__).dirpath()] def pytest_addoption(parser): parser.addoption('--gx', action="append", dest="gspecs", default=None, help=("add a global test environment, XSpec-syntax. ")) def pytest_funcarg__specssh(request): return getspecssh(request.config) def getgspecs(config): return [execnet.XSpec(spec) for spec in config.getvalueorskip("gspecs")] # configuration information for tests def getgspecs(config): return [execnet.XSpec(spec) for spec in config.getvalueorskip("gspecs")] def getspecssh(config): xspecs = getgspecs(config) for spec in xspecs: if spec.ssh: if not py.path.local.sysfind("ssh"): py.test.skip("command not found: ssh") return str(spec) py.test.skip("need '--gx ssh=...'") def getsocketspec(config): xspecs = getgspecs(config) for spec in xspecs: if spec.socket: return spec py.test.skip("need '--gx socket=...'") pytest-xdist-1.8/testing/acceptance_test.py0000644000000000000000000003220011672631266017724 0ustar rootrootimport py import sys class TestDistribution: def test_n1_pass(self, testdir): p1 = testdir.makepyfile(""" def test_ok(): pass """) result = testdir.runpytest(p1, "-n1") assert result.ret == 0 result.stdout.fnmatch_lines([ "*1 passed*", ]) def test_n1_fail(self, testdir): p1 = testdir.makepyfile(""" def test_fail(): assert 0 """) result = testdir.runpytest(p1, "-n1") assert result.ret == 1 result.stdout.fnmatch_lines([ "*1 failed*", ]) def test_n1_skip(self, testdir): p1 = testdir.makepyfile(""" def test_skip(): import py py.test.skip("myreason") """) result = testdir.runpytest(p1, "-n1") assert result.ret == 0 result.stdout.fnmatch_lines([ "*1 skipped*", ]) def test_manytests_to_one_popen(self, testdir): p1 = testdir.makepyfile(""" import py def test_fail0(): assert 0 def test_fail1(): raise ValueError() def test_ok(): pass def test_skip(): py.test.skip("hello") """, ) result = testdir.runpytest(p1, "-v", '-d', '--tx=popen', '--tx=popen') result.stdout.fnmatch_lines([ "*1*Python*", "*2 failed, 1 passed, 1 skipped*", ]) assert result.ret == 1 def test_n1_fail_minus_x(self, testdir): p1 = testdir.makepyfile(""" def test_fail1(): assert 0 def test_fail2(): assert 0 """) result = testdir.runpytest(p1, "-x", "-v", "-n1") assert result.ret == 2 result.stdout.fnmatch_lines([ "*Interrupted: stopping*1*", "*1 failed*", ]) def test_basetemp_in_subprocesses(self, testdir): p1 = testdir.makepyfile(""" def test_send(tmpdir): import py assert tmpdir.relto(py.path.local(%r)), tmpdir """ % str(testdir.tmpdir)) result = testdir.runpytest(p1, "-n1") assert result.ret == 0 result.stdout.fnmatch_lines([ "*1 passed*", ]) def test_dist_ini_specified(self, testdir): p1 = testdir.makepyfile(""" import py def test_fail0(): assert 0 def test_fail1(): raise ValueError() def test_ok(): pass def test_skip(): py.test.skip("hello") """, ) testdir.makeini(""" [pytest] addopts = --tx=3*popen """) result = testdir.runpytest(p1, '-d', "-v") result.stdout.fnmatch_lines([ "*2*Python*", "*2 failed, 1 passed, 1 skipped*", ]) assert result.ret == 1 @py.test.mark.xfail("sys.platform.startswith('java')", run=False) def test_dist_tests_with_crash(self, testdir): if not hasattr(py.std.os, 'kill'): py.test.skip("no os.kill") p1 = testdir.makepyfile(""" import py def test_fail0(): assert 0 def test_fail1(): raise ValueError() def test_ok(): pass def test_skip(): py.test.skip("hello") def test_crash(): import time import os time.sleep(0.5) os.kill(os.getpid(), 15) """ ) result = testdir.runpytest(p1, "-v", '-d', '-n1') result.stdout.fnmatch_lines([ "*Python*", "*PASS**test_ok*", "*node*down*", "*3 failed, 1 passed, 1 skipped*" ]) assert result.ret == 1 def test_distribution_rsyncdirs_example(self, testdir): source = testdir.mkdir("source") dest = testdir.mkdir("dest") subdir = source.mkdir("example_pkg") subdir.ensure("__init__.py") p = subdir.join("test_one.py") p.write("def test_5():\n assert not __file__.startswith(%r)" % str(p)) result = testdir.runpytest("-v", "-d", "--rsyncdir=%(subdir)s" % locals(), "--tx=popen//chdir=%(dest)s" % locals(), p) assert result.ret == 0 result.stdout.fnmatch_lines([ "*0* *cwd*", #"RSyncStart: [G1]", #"RSyncFinished: [G1]", "*1 passed*" ]) assert dest.join(subdir.basename).check(dir=1) def test_data_exchange(self, testdir): c1 = testdir.makeconftest(""" # This hook only called on master. def pytest_configure_node(node): node.slaveinput['a'] = 42 node.slaveinput['b'] = 7 def pytest_configure(config): # this attribute is only set on slaves if hasattr(config, 'slaveinput'): a = config.slaveinput['a'] b = config.slaveinput['b'] r = a + b config.slaveoutput['r'] = r # This hook only called on master. def pytest_testnodedown(node, error): node.config.calc_result = node.slaveoutput['r'] def pytest_terminal_summary(terminalreporter): if not hasattr(terminalreporter.config, 'slaveinput'): calc_result = terminalreporter.config.calc_result terminalreporter._tw.sep('-', 'calculated result is %s' % calc_result) """) p1 = testdir.makepyfile("def test_func(): pass") result = testdir.runpytest("-v", p1, '-d', '--tx=popen') result.stdout.fnmatch_lines([ "*0*Python*", "*calculated result is 49*", "*1 passed*" ]) assert result.ret == 0 def test_keyboardinterrupt_hooks_issue79(self, testdir): testdir.makepyfile(__init__="", test_one=""" def test_hello(): raise KeyboardInterrupt() """) testdir.makeconftest(""" def pytest_sessionfinish(session): # on the slave if hasattr(session.config, 'slaveoutput'): session.config.slaveoutput['s2'] = 42 # on the master def pytest_testnodedown(node, error): assert node.slaveoutput['s2'] == 42 print ("s2call-finished") """) args = ["-n1", "--debug"] result = testdir.runpytest(*args) s = result.stdout.str() assert result.ret == 2 assert 's2call' in s assert "Interrupted" in s def test_keyboard_interrupt_dist(self, testdir): # xxx could be refined to check for return code p = testdir.makepyfile(""" def test_sleep(): import time time.sleep(10) """) child = testdir.spawn_pytest("-n1") child.expect(".*test session starts.*") child.kill(2) # keyboard interrupt child.expect(".*KeyboardInterrupt.*") #child.expect(".*seconds.*") child.close() #assert ret == 2 class TestDistEach: def test_simple(self, testdir): testdir.makepyfile(""" def test_hello(): pass """) result = testdir.runpytest("--debug", "--dist=each", "--tx=2*popen") assert not result.ret result.stdout.fnmatch_lines(["*2 pass*"]) @py.test.mark.xfail(run=False, reason="other python versions might not have py.test installed") def test_simple_diffoutput(self, testdir): interpreters = [] for name in ("python2.5", "python2.6"): interp = py.path.local.sysfind(name) if interp is None: py.test.skip("%s not found" % name) interpreters.append(interp) testdir.makepyfile(__init__="", test_one=""" import sys def test_hello(): print("%s...%s" % sys.version_info[:2]) assert 0 """) args = ["--dist=each", "-v"] args += ["--tx", "popen//python=%s" % interpreters[0]] args += ["--tx", "popen//python=%s" % interpreters[1]] result = testdir.runpytest(*args) s = result.stdout.str() assert "2...5" in s assert "2...6" in s class TestTerminalReporting: def test_pass_skip_fail(self, testdir): p = testdir.makepyfile(""" import py def test_ok(): pass def test_skip(): py.test.skip("xx") def test_func(): assert 0 """) result = testdir.runpytest("-n1", "-v") result.stdout.fnmatch_lines_random([ "*PASS*test_pass_skip_fail.py:2: *test_ok*", "*SKIP*test_pass_skip_fail.py:4: *test_skip*", "*FAIL*test_pass_skip_fail.py:6: *test_func*", ]) result.stdout.fnmatch_lines([ "*def test_func():", "> assert 0", "E assert 0", ]) def test_fail_platinfo(self, testdir): p = testdir.makepyfile(""" def test_func(): assert 0 """) result = testdir.runpytest("-n1", "-v") result.stdout.fnmatch_lines([ "*FAIL*test_fail_platinfo.py:1: *test_func*", "*0*Python*", "*def test_func():", "> assert 0", "E assert 0", ]) def test_teardownfails_one_function(testdir): p = testdir.makepyfile(""" def test_func(): pass def teardown_function(function): assert 0 """) result = testdir.runpytest(p, '-n1', '--tx=popen') result.stdout.fnmatch_lines([ "*def teardown_function(function):*", "*1 passed*1 error*" ]) @py.test.mark.xfail def test_terminate_on_hangingnode(testdir): p = testdir.makeconftest(""" def pytest_sessionfinishes(session): if session.nodeid == "my": # running on slave import time time.sleep(3) """) result = testdir.runpytest(p, '--dist=each', '--tx=popen//id=my') assert result.duration < 2.0 result.stdout.fnmatch_lines([ "*killed*my*", ]) def test_session_hooks(testdir): testdir.makeconftest(""" import sys def pytest_sessionstart(session): sys.pytestsessionhooks = session def pytest_sessionfinish(session): if hasattr(session.config, 'slaveinput'): name = "slave" else: name = "master" f = open(name, "w") f.write("xy") f.close() # let's fail on the slave if name == "slave": raise ValueError(42) """) p = testdir.makepyfile(""" import sys def test_hello(): assert hasattr(sys, 'pytestsessionhooks') """) result = testdir.runpytest(p, "--dist=each", "--tx=popen") result.stdout.fnmatch_lines([ "*ValueError*", "*1 passed*", ]) assert not result.ret d = result.parseoutcomes() assert d['passed'] == 1 assert testdir.tmpdir.join("slave").check() assert testdir.tmpdir.join("master").check() def test_funcarg_teardown_failure(testdir): p = testdir.makepyfile(""" def pytest_funcarg__myarg(request): def teardown(val): raise ValueError(val) return request.cached_setup(setup=lambda: 42, teardown=teardown, scope="module") def test_hello(myarg): pass """) result = testdir.runpytest("--debug", p) # , "-n1") result.stdout.fnmatch_lines([ "*ValueError*42*", "*1 passed*1 error*", ]) assert result.ret def test_crashing_item(testdir): p = testdir.makepyfile(""" import py import os def test_crash(): py.process.kill(os.getpid()) def test_noncrash(): pass """) result = testdir.runpytest("-n2", p) result.stdout.fnmatch_lines([ "*crashed*test_crash*", "*1 failed*1 passed*" ]) def test_skipping(testdir): p = testdir.makepyfile(""" import pytest def test_crash(): pytest.skip("hello") """) result = testdir.runpytest("-n1", '-rs', p) assert result.ret == 0 result.stdout.fnmatch_lines([ "*hello*", "*1 skipped*" ]) def test_issue34_pluginloading_in_subprocess(testdir): testdir.tmpdir.join("plugin123.py").write(py.code.Source(""" def pytest_namespace(): return {'sample_variable': 'testing'} """)) testdir.makepyfile(""" import pytest def test_hello(): assert pytest.sample_variable == "testing" """) result = testdir.runpytest("-n1", "-p", "plugin123") assert result.ret == 0 result.stdout.fnmatch_lines([ "*1 passed*", ]) pytest-xdist-1.8/pytest_xdist.egg-info/0000755000000000000000000000000011760166052017002 5ustar rootrootpytest-xdist-1.8/pytest_xdist.egg-info/top_level.txt0000644000000000000000000000000611672631274021535 0ustar rootrootxdist pytest-xdist-1.8/pytest_xdist.egg-info/SOURCES.txt0000644000000000000000000000116011672631274020671 0ustar rootrootCHANGELOG LICENSE MANIFEST.in README.txt setup.py pytest_xdist.egg-info/PKG-INFO pytest_xdist.egg-info/SOURCES.txt pytest_xdist.egg-info/dependency_links.txt pytest_xdist.egg-info/entry_points.txt pytest_xdist.egg-info/not-zip-safe pytest_xdist.egg-info/requires.txt pytest_xdist.egg-info/top_level.txt testing/acceptance_test.py testing/conftest.py testing/test_boxed.py testing/test_dsession.py testing/test_looponfail.py testing/test_plugin.py testing/test_remote.py testing/test_slavemanage.py xdist/__init__.py xdist/dsession.py xdist/looponfail.py xdist/newhooks.py xdist/plugin.py xdist/remote.py xdist/slavemanage.pypytest-xdist-1.8/pytest_xdist.egg-info/requires.txt0000644000000000000000000000003411672631274021404 0ustar rootrootexecnet>=1.0.8 pytest>=2.2.1pytest-xdist-1.8/pytest_xdist.egg-info/PKG-INFO0000644000000000000000000001670711672631274020117 0ustar rootrootMetadata-Version: 1.0 Name: pytest-xdist Version: 1.8 Summary: py.test xdist plugin for distributed testing and loop-on-failing modes Home-page: http://bitbucket.org/hpk42/pytest-xdist Author: holger krekel and contributors Author-email: py-dev@codespeak.net,holger@merlinux.eu License: GPLv2 or later Description: xdist: pytest distributed testing plugin =============================================================== The `pytest-xdist`_ plugin extends py.test with some unique test execution modes: * Looponfail: run your tests repeatedly in a subprocess. After each run py.test waits until a file in your project changes and then re-runs the previously failing tests. This is repeated until all tests pass after which again a full run is performed. * multiprocess Load-balancing: if you have multiple CPUs or hosts you can use those for a combined test run. This allows to speed up development or to use special resources of remote machines. * Multi-Platform coverage: you can specify different Python interpreters or different platforms and run tests in parallel on all of them. Before running tests remotely, ``py.test`` efficiently "rsyncs" your program source code to the remote place. All test results are reported back and displayed to your local terminal. You may specify different Python versions and interpreters. Installation ----------------------- Install the plugin with:: easy_install pytest-xdist # or pip install pytest-xdist or use the package in develope/in-place mode with a checkout of the `pytest-xdist repository`_ :: python setup.py develop Usage examples --------------------- Speed up test runs by sending tests to multiple CPUs +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ To send tests to multiple CPUs, type:: py.test -n NUM Especially for longer running tests or tests requiring a lot of IO this can lead to considerable speed ups. Running tests in a Python subprocess +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ To instantiate a python2.4 sub process and send tests to it, you may type:: py.test -d --tx popen//python=python2.4 This will start a subprocess which is run with the "python2.4" Python interpreter, found in your system binary lookup path. If you prefix the --tx option value like this:: --tx 3*popen//python=python2.4 then three subprocesses would be created and tests will be load-balanced across these three processes. Sending tests to remote SSH accounts +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Suppose you have a package ``mypkg`` which contains some tests that you can successfully run locally. And you have a ssh-reachable machine ``myhost``. Then you can ad-hoc distribute your tests by typing:: py.test -d --tx ssh=myhostpopen --rsyncdir mypkg mypkg This will synchronize your ``mypkg`` package directory to an remote ssh account and then locally collect tests and send them to remote places for execution. You can specify multiple ``--rsyncdir`` directories to be sent to the remote side. **NOTE:** For py.test to collect and send tests correctly you not only need to make sure all code and tests directories are rsynced, but that any test (sub) directory also has an ``__init__.py`` file because internally py.test references tests as a fully qualified python module path. **You will otherwise get strange errors** during setup of the remote side. Sending tests to remote Socket Servers +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Download the single-module `socketserver.py`_ Python program and run it like this:: python socketserver.py It will tell you that it starts listening on the default port. You can now on your home machine specify this new socket host with something like this:: py.test -d --tx socket=192.168.1.102:8888 --rsyncdir mypkg mypkg .. _`atonce`: Running tests on many platforms at once +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ The basic command to run tests on multiple platforms is:: py.test --dist=each --tx=spec1 --tx=spec2 If you specify a windows host, an OSX host and a Linux environment this command will send each tests to all platforms - and report back failures from all platforms at once. The specifications strings use the `xspec syntax`_. .. _`xspec syntax`: http://codespeak.net/execnet/trunk/basics.html#xspec .. _`socketserver.py`: http://bitbucket.org/hpk42/execnet/raw/2af991418160/execnet/script/socketserver.py .. _`execnet`: http://codespeak.net/execnet Specifying test exec environments in an ini file +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ pytest (since version 2.0) supports ini-style cofiguration. You can for example make running with three subprocesses your default like this:: [pytest] addopts = -n3 You can also add default environments like this:: [pytest] addopts = --tx ssh=myhost//python=python2.5 --tx ssh=myhost//python=python2.6 and then just type:: py.test --dist=each to run tests in each of the environments. Specifying "rsync" dirs in an ini-file +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ In a ``tox.ini`` or ``setup.cfg`` file in your root project directory you may specify directories to include or to exclude in synchronisation:: [pytest] rsyncdirs = . mypkg helperpkg rsyncignore = .hg These directory specifications are relative to the directory where the configuration file was found. .. _`pytest-xdist`: http://pypi.python.org/pypi/pytest-xdist .. _`pytest-xdist repository`: http://bitbucket.org/hpk42/pytest-xdist .. _`pytest`: http://pytest.org Platform: linux Platform: osx Platform: win32 Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU General Public License (GPL) Classifier: Operating System :: POSIX Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: MacOS :: MacOS X Classifier: Topic :: Software Development :: Testing Classifier: Topic :: Software Development :: Quality Assurance Classifier: Topic :: Utilities Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 pytest-xdist-1.8/pytest_xdist.egg-info/not-zip-safe0000644000000000000000000000000111672631270021231 0ustar rootroot pytest-xdist-1.8/pytest_xdist.egg-info/entry_points.txt0000644000000000000000000000004111672631274022300 0ustar rootroot[pytest11] xdist = xdist.plugin pytest-xdist-1.8/pytest_xdist.egg-info/dependency_links.txt0000644000000000000000000000000111672631274023055 0ustar rootroot pytest-xdist-1.8/setup.py0000644000000000000000000000215611672631266014271 0ustar rootrootfrom setuptools import setup setup( name="pytest-xdist", version='1.8', description='py.test xdist plugin for distributed testing and loop-on-failing modes', long_description=open('README.txt').read(), license='GPLv2 or later', author='holger krekel and contributors', author_email='py-dev@codespeak.net,holger@merlinux.eu', url='http://bitbucket.org/hpk42/pytest-xdist', platforms=['linux', 'osx', 'win32'], packages = ['xdist'], entry_points = {'pytest11': ['xdist = xdist.plugin'],}, zip_safe=False, install_requires = ['execnet>=1.0.8', 'pytest>=2.2.1'], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: GNU General Public License (GPL)', 'Operating System :: POSIX', 'Operating System :: Microsoft :: Windows', 'Operating System :: MacOS :: MacOS X', 'Topic :: Software Development :: Testing', 'Topic :: Software Development :: Quality Assurance', 'Topic :: Utilities', 'Programming Language :: Python', 'Programming Language :: Python :: 3', ], ) pytest-xdist-1.8/setup.cfg0000644000000000000000000000007311672631274014373 0ustar rootroot[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 pytest-xdist-1.8/README.txt0000644000000000000000000001234711672631266014260 0ustar rootrootxdist: pytest distributed testing plugin =============================================================== The `pytest-xdist`_ plugin extends py.test with some unique test execution modes: * Looponfail: run your tests repeatedly in a subprocess. After each run py.test waits until a file in your project changes and then re-runs the previously failing tests. This is repeated until all tests pass after which again a full run is performed. * multiprocess Load-balancing: if you have multiple CPUs or hosts you can use those for a combined test run. This allows to speed up development or to use special resources of remote machines. * Multi-Platform coverage: you can specify different Python interpreters or different platforms and run tests in parallel on all of them. Before running tests remotely, ``py.test`` efficiently "rsyncs" your program source code to the remote place. All test results are reported back and displayed to your local terminal. You may specify different Python versions and interpreters. Installation ----------------------- Install the plugin with:: easy_install pytest-xdist # or pip install pytest-xdist or use the package in develope/in-place mode with a checkout of the `pytest-xdist repository`_ :: python setup.py develop Usage examples --------------------- Speed up test runs by sending tests to multiple CPUs +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ To send tests to multiple CPUs, type:: py.test -n NUM Especially for longer running tests or tests requiring a lot of IO this can lead to considerable speed ups. Running tests in a Python subprocess +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ To instantiate a python2.4 sub process and send tests to it, you may type:: py.test -d --tx popen//python=python2.4 This will start a subprocess which is run with the "python2.4" Python interpreter, found in your system binary lookup path. If you prefix the --tx option value like this:: --tx 3*popen//python=python2.4 then three subprocesses would be created and tests will be load-balanced across these three processes. Sending tests to remote SSH accounts +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Suppose you have a package ``mypkg`` which contains some tests that you can successfully run locally. And you have a ssh-reachable machine ``myhost``. Then you can ad-hoc distribute your tests by typing:: py.test -d --tx ssh=myhostpopen --rsyncdir mypkg mypkg This will synchronize your ``mypkg`` package directory to an remote ssh account and then locally collect tests and send them to remote places for execution. You can specify multiple ``--rsyncdir`` directories to be sent to the remote side. **NOTE:** For py.test to collect and send tests correctly you not only need to make sure all code and tests directories are rsynced, but that any test (sub) directory also has an ``__init__.py`` file because internally py.test references tests as a fully qualified python module path. **You will otherwise get strange errors** during setup of the remote side. Sending tests to remote Socket Servers +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Download the single-module `socketserver.py`_ Python program and run it like this:: python socketserver.py It will tell you that it starts listening on the default port. You can now on your home machine specify this new socket host with something like this:: py.test -d --tx socket=192.168.1.102:8888 --rsyncdir mypkg mypkg .. _`atonce`: Running tests on many platforms at once +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ The basic command to run tests on multiple platforms is:: py.test --dist=each --tx=spec1 --tx=spec2 If you specify a windows host, an OSX host and a Linux environment this command will send each tests to all platforms - and report back failures from all platforms at once. The specifications strings use the `xspec syntax`_. .. _`xspec syntax`: http://codespeak.net/execnet/trunk/basics.html#xspec .. _`socketserver.py`: http://bitbucket.org/hpk42/execnet/raw/2af991418160/execnet/script/socketserver.py .. _`execnet`: http://codespeak.net/execnet Specifying test exec environments in an ini file +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ pytest (since version 2.0) supports ini-style cofiguration. You can for example make running with three subprocesses your default like this:: [pytest] addopts = -n3 You can also add default environments like this:: [pytest] addopts = --tx ssh=myhost//python=python2.5 --tx ssh=myhost//python=python2.6 and then just type:: py.test --dist=each to run tests in each of the environments. Specifying "rsync" dirs in an ini-file +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ In a ``tox.ini`` or ``setup.cfg`` file in your root project directory you may specify directories to include or to exclude in synchronisation:: [pytest] rsyncdirs = . mypkg helperpkg rsyncignore = .hg These directory specifications are relative to the directory where the configuration file was found. .. _`pytest-xdist`: http://pypi.python.org/pypi/pytest-xdist .. _`pytest-xdist repository`: http://bitbucket.org/hpk42/pytest-xdist .. _`pytest`: http://pytest.org pytest-xdist-1.8/PKG-INFO0000644000000000000000000001670711672631274013662 0ustar rootrootMetadata-Version: 1.0 Name: pytest-xdist Version: 1.8 Summary: py.test xdist plugin for distributed testing and loop-on-failing modes Home-page: http://bitbucket.org/hpk42/pytest-xdist Author: holger krekel and contributors Author-email: py-dev@codespeak.net,holger@merlinux.eu License: GPLv2 or later Description: xdist: pytest distributed testing plugin =============================================================== The `pytest-xdist`_ plugin extends py.test with some unique test execution modes: * Looponfail: run your tests repeatedly in a subprocess. After each run py.test waits until a file in your project changes and then re-runs the previously failing tests. This is repeated until all tests pass after which again a full run is performed. * multiprocess Load-balancing: if you have multiple CPUs or hosts you can use those for a combined test run. This allows to speed up development or to use special resources of remote machines. * Multi-Platform coverage: you can specify different Python interpreters or different platforms and run tests in parallel on all of them. Before running tests remotely, ``py.test`` efficiently "rsyncs" your program source code to the remote place. All test results are reported back and displayed to your local terminal. You may specify different Python versions and interpreters. Installation ----------------------- Install the plugin with:: easy_install pytest-xdist # or pip install pytest-xdist or use the package in develope/in-place mode with a checkout of the `pytest-xdist repository`_ :: python setup.py develop Usage examples --------------------- Speed up test runs by sending tests to multiple CPUs +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ To send tests to multiple CPUs, type:: py.test -n NUM Especially for longer running tests or tests requiring a lot of IO this can lead to considerable speed ups. Running tests in a Python subprocess +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ To instantiate a python2.4 sub process and send tests to it, you may type:: py.test -d --tx popen//python=python2.4 This will start a subprocess which is run with the "python2.4" Python interpreter, found in your system binary lookup path. If you prefix the --tx option value like this:: --tx 3*popen//python=python2.4 then three subprocesses would be created and tests will be load-balanced across these three processes. Sending tests to remote SSH accounts +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Suppose you have a package ``mypkg`` which contains some tests that you can successfully run locally. And you have a ssh-reachable machine ``myhost``. Then you can ad-hoc distribute your tests by typing:: py.test -d --tx ssh=myhostpopen --rsyncdir mypkg mypkg This will synchronize your ``mypkg`` package directory to an remote ssh account and then locally collect tests and send them to remote places for execution. You can specify multiple ``--rsyncdir`` directories to be sent to the remote side. **NOTE:** For py.test to collect and send tests correctly you not only need to make sure all code and tests directories are rsynced, but that any test (sub) directory also has an ``__init__.py`` file because internally py.test references tests as a fully qualified python module path. **You will otherwise get strange errors** during setup of the remote side. Sending tests to remote Socket Servers +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Download the single-module `socketserver.py`_ Python program and run it like this:: python socketserver.py It will tell you that it starts listening on the default port. You can now on your home machine specify this new socket host with something like this:: py.test -d --tx socket=192.168.1.102:8888 --rsyncdir mypkg mypkg .. _`atonce`: Running tests on many platforms at once +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ The basic command to run tests on multiple platforms is:: py.test --dist=each --tx=spec1 --tx=spec2 If you specify a windows host, an OSX host and a Linux environment this command will send each tests to all platforms - and report back failures from all platforms at once. The specifications strings use the `xspec syntax`_. .. _`xspec syntax`: http://codespeak.net/execnet/trunk/basics.html#xspec .. _`socketserver.py`: http://bitbucket.org/hpk42/execnet/raw/2af991418160/execnet/script/socketserver.py .. _`execnet`: http://codespeak.net/execnet Specifying test exec environments in an ini file +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ pytest (since version 2.0) supports ini-style cofiguration. You can for example make running with three subprocesses your default like this:: [pytest] addopts = -n3 You can also add default environments like this:: [pytest] addopts = --tx ssh=myhost//python=python2.5 --tx ssh=myhost//python=python2.6 and then just type:: py.test --dist=each to run tests in each of the environments. Specifying "rsync" dirs in an ini-file +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ In a ``tox.ini`` or ``setup.cfg`` file in your root project directory you may specify directories to include or to exclude in synchronisation:: [pytest] rsyncdirs = . mypkg helperpkg rsyncignore = .hg These directory specifications are relative to the directory where the configuration file was found. .. _`pytest-xdist`: http://pypi.python.org/pypi/pytest-xdist .. _`pytest-xdist repository`: http://bitbucket.org/hpk42/pytest-xdist .. _`pytest`: http://pytest.org Platform: linux Platform: osx Platform: win32 Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU General Public License (GPL) Classifier: Operating System :: POSIX Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: MacOS :: MacOS X Classifier: Topic :: Software Development :: Testing Classifier: Topic :: Software Development :: Quality Assurance Classifier: Topic :: Utilities Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 pytest-xdist-1.8/MANIFEST.in0000644000000000000000000000014011672631266014304 0ustar rootrootinclude CHANGELOG include LICENSE include README.txt include setup.py graft testing prune .hg pytest-xdist-1.8/LICENSE0000644000000000000000000000067211672631266013565 0ustar rootrootThe execnet package is released under the provisions of the Gnu Public License (GPL), version 2 or later. See http://www.fsf.org/licensing/licenses/ for more information. This package also contains some minor parts which which are useable under the MIT license. If you have questions and/or want to use parts of the code under a different license than the GPL please contact me. holger krekel, January 2010, holger at merlinux eu pytest-xdist-1.8/CHANGELOG0000644000000000000000000000545111672631266013772 0ustar rootroot1.8 ------------------------- - fix pytest-issue93 - use the refined pytest-2.2.1 runtestprotocol interface to perform eager teardowns for test items. 1.7 ------------------------- - fix incompatibilities with pytest-2.2.0 (allow multiple pytest_runtest_logreport reports for a test item) 1.6 ------------------------- - terser collection reporting - fix issue34 - distributed testing with -p plugin now works correctly - fix race condition in looponfail mode where a concurrent file removal could cause a crash 1.5 ------------------------- - adapt to and require pytest-2.0 changes, rsyncdirs and rsyncignore can now only be specified in [pytest] sections of ini files, see "py.test -h" for details. - major internal refactoring to match the pytest-2.0 event refactoring - perform test collection always at slave side instead of at the master - make python2/python3 bridging work, remove usage of pickling - improve initial reporting by using line-rewriting - remove all trailing whitespace from source 1.4 ------------------------- - perform distributed testing related reporting in the plugin rather than having dist-related code in the generic py.test distribution - depend on execnet-1.0.7 which adds "env1:NAME=value" keys to gateway specification strings. - show detailed gateway setup and platform information only when "-v" or "--verbose" is specified. 1.3 ------------------------- - fix --looponfailing - it would not actually run against the fully changed source tree when initial conftest files load application state. - adapt for py-1.3.1's new --maxfailure option 1.2 ------------------------- - fix issue79: sessionfinish/teardown hooks are now called systematically on the slave side - introduce a new data input/output mechanism to allow the master side to send and receive data from a slave. - fix race condition in underlying pickling/unpickling handling - use and require new register hooks facility of py.test>=1.3.0 - require improved execnet>=1.0.6 because of various race conditions that can arise in xdist testing modes. - fix some python3 related pickling related race conditions - fix PyPI description 1.1 ------------------------- - fix an indefinite hang which would wait for events although no events are pending - this happened if items arrive very quickly while the "reschedule-event" tried unconditionally avoiding a busy-loop and not schedule new work. 1.0 ------------------------- - moved code out of py-1.1.1 into its own plugin - use a new, faster and more sensible model to do load-balancing of tests - now no magic "MAXITEMSPERHOST" is needed and load-testing works effectively even with very few tests. - cleaned up termination handling - make -x cause hard killing of test nodes to decrease wait time until the traceback shows up on first failure