Forked from
Core Modules / dune-common
1588 commits behind the upstream repository.
-
Steffen Müthing authoredSteffen Müthing authored
dune-ctest 8.37 KiB
#! /usr/bin/env python3
#
# Wrapper around CTest for DUNE
#
# CTest returns with an error status not only when tests failed, but also
# when tests were only skipped. This wrapper checks the log and returns
# successfully if no tests failed; skipped tests do not result in an error.
# This behaviour is needed in a continuous integration environment, when
# building binary packages or in other cases where the testsuite should be
# run automatically.
#
# Moreover, this script also converts the XML test report generated by CTest
# into a JUnit report file that can be consumed by a lot of reporting
# software.
#
# Author: Ansgar Burchardt <Ansgar.Burchardt@tu-dresden.de>
# Author: Steffen Müthing <steffen.muething@iwr.uni-heidelberg.de> (for the JUnit part)
import errno
import glob
import os.path
import shutil
import subprocess
import sys
import xml.etree.ElementTree as et
from pathlib import Path
import os
import re
class CTestParser:
def findCTestOutput(self):
files = glob.glob("Testing/*/Test.xml")
if len(files) != 1:
fn = files.join(", ")
raise Exception("Found multiple CTest output files: {}".format(files.join(", ")))
return files[0]
def printTest(self,test,output=None):
status = test.get("Status")
name = test.find("Name").text
fullName = test.find("FullName").text
if output is not None:
output = test.find("Results").find("Measurement").find("Value").text
print("======================================================================")
print("Name: {}".format(name))
print("FullName: {}".format(fullName))
print("Status: {}".format(status.upper()))
if output:
print("Output:")
for line in output.splitlines():
print(" ", line)
print()
def __init__(self,junitpath=None):
self.inputpath = self.findCTestOutput()
if junitpath is None:
if "CI_PROJECT_DIR" in os.environ:
buildroot = Path(os.environ["CI_PROJECT_DIR"])
# create a slug from the project name
name = os.environ["CI_PROJECT_NAME"].lower()
name = re.sub(r"[^-a-z0-9]","-",name);
junitbasename = "{}-".format(name)
else:
buildroot = Path.cwd()
junitbasename = ""
junitdir = buildroot / "junit"
junitdir.mkdir(parents=True,exist_ok=True)
self.junitpath = junitdir / "{}cmake.xml".format(junitbasename)
else:
self.junitpath = Path(junitpath)
junitdir = junitpath.resolve().parent
junitdir.mkdir(parents=True,exist_ok=True)
self.tests = 0
self.passed = 0
self.failures = 0
self.skipped = 0
self.errors = 0
self.skipped = 0
self.time = 0.0
def createJUnitSkeleton(self):
self.testsuites = et.Element("testsuites")
self.testsuite = et.SubElement(self.testsuites,"testsuite")
self.properties = et.SubElement(self.testsuite,"properties")
def fillJUnitStatistics(self):
self.testsuite.set("name","cmake")
self.testsuite.set("tests",str(self.tests))
self.testsuite.set("disabled","0")
self.testsuite.set("errors",str(self.errors))
self.testsuite.set("failures",str(self.failures))
self.testsuite.set("skipped",str(self.skipped))
self.testsuite.set("time",str(self.time))
def processTest(self,test):
testcase = et.SubElement(self.testsuite,"testcase")
testcase.set("name",test.find("Name").text)
testcase.set("assertions","1")
testcase.set("classname","cmake")
time = test.find("./Results/NamedMeasurement[@name='Execution Time']/Value")
if time is not None:
self.time += float(time.text)
testcase.set("time",time.text)
self.tests += 1
outcome = test.get("Status")
if outcome == "passed":
testcase.set("status","passed")
self.passed += 1
elif outcome == "failed":
self.failures += 1
testcase.set("status","failure")
failure = et.SubElement(testcase,"failure")
failure.set("message","program execution failed")
failure.text = test.find("./Results/Measurement/Value").text
self.printTest(test)
elif outcome == "notrun":
# This does not exit on older CMake versions, so work around that
try:
status = test.find("./Results/NamedMeasurement[@name='Completion Status']/Value").text
if status == "SKIP_RETURN_CODE=77":
self.skipped += 1
et.SubElement(testcase,"skipped")
elif status == "Required Files Missing":
self.errors += 1
error = et.SubElement(testcase,"error")
error.set("message","compilation failed")
error.set("type","compilation error")
self.printTest(test,output="Compilation error")
else:
error = et.SubElement(testcase,"error")
error.set("message","unknown error during test execution")
error.set("type","unknown")
error.text = test.find("./Results/Measurement/Value").text
self.errors += 1
self.printTest(test)
except AttributeError:
output_tag = test.find("./Results/Measurement/Value")
if output_tag is not None:
msg = output_tag.text
if "skipped" in msg:
self.skipped += 1
et.SubElement(testcase,"skipped")
elif "Unable to find required file" in msg:
self.errors += 1
error = et.SubElement(testcase,"error")
error.set("message","compilation failed")
error.set("type","compilation error")
self.printTest(test,output="Compilation error")
else:
error = et.SubElement(testcase,"error")
error.set("message","unknown error during test execution")
error.set("type","unknown")
error.text = msg
self.errors += 1
self.printTest(test)
else:
error = et.SubElement(testcase,"error")
error.set("message","unknown error during test execution")
error.set("type","unknown")
error.text = "no message"
self.errors += 1
self.printTest(test)
output_tag = test.find("./Results/Measurement/Value")
if output_tag is not None:
out = et.SubElement(testcase,"system-out")
out.text = output_tag.text
def process(self):
with open(self.inputpath, "r", encoding="utf-8") as fh:
tree = et.parse(fh)
root = tree.getroot()
self.createJUnitSkeleton()
for test in root.findall(".//Testing/Test"):
self.processTest(test)
self.fillJUnitStatistics()
with self.junitpath.open("wb") as fh:
fh.write(et.tostring(self.testsuites,encoding="utf-8"))
print("JUnit report for CTest results written to {}".format(self.junitpath))
return self.errors + self.failures
def runCTest(argv=[]):
cmd = ["ctest",
"--output-on-failure",
"--dashboard", "ExperimentalTest",
"--no-compress-output",
]
cmd.extend(argv)
subprocess.call(cmd)
def checkDirectory():
if not os.path.exists("CMakeCache.txt"):
raise Exception("ERROR: dune-ctest must be run in a cmake build directory")
def removeCTestOutput():
try:
shutil.rmtree("Testing")
except OSError as e:
if e.errno != errno.ENOENT:
raise
def main():
try:
checkDirectory()
removeCTestOutput()
runCTest(argv=sys.argv[1:])
parser = CTestParser()
errors = parser.process()
status = 0 if errors == 0 else 1
sys.exit(status)
except Exception as e:
print("Internal error: {}".format(e))
sys.exit(127)
if __name__ == "__main__":
main()