diff --git a/bin-for-dist/hudson.py b/bin-for-dist/hudson.py
new file mode 100644
index 000000000..d5f8d6498
--- /dev/null
+++ b/bin-for-dist/hudson.py
@@ -0,0 +1,21 @@
+import os, sys, subprocess, urllib
+
+BUILDNAME=os.environ.get('BUILD_NUMBER', "1")
+BIN=os.path.abspath(os.path.join(BUILDNAME, 'bin'))
+PYTHON=os.path.join(BIN, 'python')
+
+def call(*args):
+ ret = subprocess.call(list(args))
+ assert ret == 0
+
+def bincall(*args):
+ args = list(args)
+ args[0] = os.path.join(BIN, args[0])
+ call(*args)
+
+call("virtualenv", BUILDNAME, '--no-site-packages')
+bincall("python", "setup.py", "develop", "-q")
+bincall("pip", "install", "-r", "testing/pip-reqs1.txt",
+ "-q", "--download-cache=download")
+bincall("py.test", "-p", "xmlresult", "--xmlresult=junit.xml",
+ "--report=skipped", "--runslowtest")
diff --git a/pytest_xmlresult.py b/pytest_xmlresult.py
new file mode 100644
index 000000000..66c7a3857
--- /dev/null
+++ b/pytest_xmlresult.py
@@ -0,0 +1,189 @@
+"""
+ xmlresult plugin for machine-readable logging of test results.
+ Useful for cruisecontrol integration code.
+
+ An adaptation of pytest_resultlog.py
+"""
+
+import time
+
+def pytest_addoption(parser):
+ group = parser.addgroup("xmlresult", "xmlresult plugin options")
+ group.addoption('--xmlresult', action="store", dest="xmlresult", metavar="path", default=None,
+ help="path for machine-readable xml result log.")
+
+def pytest_configure(config):
+ xmlresult = config.option.xmlresult
+ if xmlresult:
+ logfile = open(xmlresult, 'w', 1) # line buffered
+ config._xmlresult = XMLResult(logfile)
+ config.pluginmanager.register(config._xmlresult)
+
+def pytest_unconfigure(config):
+ xmlresult = getattr(config, '_xmlresult', None)
+ if xmlresult:
+ xmlresult.logfile.close()
+ del config._xmlresult
+ config.pluginmanager.unregister(xmlresult)
+
+def generic_path(item):
+ chain = item.listchain()
+ gpath = [chain[0].name]
+ fspath = chain[0].fspath
+ fspart = False
+ for node in chain[1:]:
+ newfspath = node.fspath
+ if newfspath == fspath:
+ if fspart:
+ gpath.append(':')
+ fspart = False
+ else:
+ gpath.append('.')
+ else:
+ gpath.append('/')
+ fspart = True
+ name = node.name
+ if name[0] in '([':
+ gpath.pop()
+ gpath.append(name)
+ fspath = newfspath
+ return ''.join(gpath)
+
+class XMLResult(object):
+ test_start_time = 0.0
+ test_taken_time = 0.0
+ test_count = 0
+ error_count = 0
+ failure_count = 0
+ skip_count = 0
+
+ def __init__(self, logfile):
+ self.logfile = logfile
+ self.test_logs = []
+
+ def write_log_entry(self, testpath, shortrepr, longrepr):
+ self.test_count += 1
+ # Create an xml log entry for the tests
+ self.test_logs.append('' % (testpath.split(':')[-1], testpath, self.test_taken_time))
+
+ # Do we have any other data to capture for Errors, Fails and Skips
+ if shortrepr in ['E', 'F', 'S']:
+
+ if shortrepr == 'E':
+ self.error_count += 1
+ elif shortrepr == 'F':
+ self.failure_count += 1
+ elif shortrepr == 'S':
+ self.skip_count += 1
+
+ tag_map = {'E': 'error', 'F': 'failure', 'S': 'skipped'}
+ self.test_logs.append("<%s>" % tag_map[shortrepr])
+
+ # Output any more information
+ for line in longrepr.splitlines():
+ self.test_logs.append("" % line)
+ self.test_logs.append("%s>" % tag_map[shortrepr])
+ self.test_logs.append("")
+
+ def log_outcome(self, node, shortrepr, longrepr):
+ self.write_log_entry(node.name, shortrepr, longrepr)
+
+ def pytest_runtest_logreport(self, report):
+ code = report.shortrepr
+ if report.passed:
+ longrepr = ""
+ code = "."
+ elif report.failed:
+ longrepr = str(report.longrepr)
+ code = "F"
+ elif report.skipped:
+ code = "S"
+ longrepr = str(report.longrepr.reprcrash.message)
+ self.log_outcome(report.item, code, longrepr)
+
+ def pytest_runtest_setup(self, item):
+ self.test_start_time = time.time()
+
+ def pytest_runtest_teardown(self, item):
+ self.test_taken_time = time.time() - self.test_start_time
+
+ def pytest_collectreport(self, report):
+ if not report.passed:
+ if report.failed:
+ code = "F"
+ else:
+ assert report.skipped
+ code = "S"
+ longrepr = str(report.longrepr.reprcrash)
+ self.log_outcome(report.collector, code, longrepr)
+
+ def pytest_internalerror(self, excrepr):
+ path = excrepr.reprcrash.path
+ self.errors += 1
+ self.write_log_entry(path, '!', str(excrepr))
+
+ def pytest_sessionstart(self, session):
+ self.suite_start_time = time.time()
+
+ def pytest_sessionfinish(self, session, exitstatus):
+ """
+ Write the xml output
+ """
+ suite_stop_time = time.time()
+ suite_time_delta = suite_stop_time - self.suite_start_time
+ self.logfile.write('')
+ self.logfile.writelines(self.test_logs)
+ self.logfile.write('')
+ self.logfile.close()
+
+
+# Tests
+def test_generic(testdir, LineMatcher):
+ testdir.plugins.append("resultlog")
+ testdir.makepyfile("""
+ import py
+ def test_pass():
+ pass
+ def test_fail():
+ assert 0
+ def test_skip():
+ py.test.skip("")
+ """)
+ testdir.runpytest("--xmlresult=result.xml")
+ lines = testdir.tmpdir.join("result.xml").readlines(cr=0)
+ LineMatcher(lines).fnmatch_lines([
+ '*testsuite errors="0" failures="1" skips="1" name="" tests="3"*'
+ ])
+ LineMatcher(lines).fnmatch_lines([
+ '**'
+ ])
+
+def test_generic_path():
+ from py.__.test.collect import Node, Item, FSCollector
+ p1 = Node('a')
+ assert p1.fspath is None
+ p2 = Node('B', parent=p1)
+ p3 = Node('()', parent = p2)
+ item = Item('c', parent = p3)
+ res = generic_path(item)
+ assert res == 'a.B().c'
+
+ p0 = FSCollector('proj/test')
+ p1 = FSCollector('proj/test/a', parent=p0)
+ p2 = Node('B', parent=p1)
+ p3 = Node('()', parent = p2)
+ p4 = Node('c', parent=p3)
+ item = Item('[1]', parent = p4)
+
+ res = generic_path(item)
+ assert res == 'test/a:B().c[1]'