Skip to content
Snippets Groups Projects
dune-ctest 8.37 KiB
Newer Older
  • Learn to ignore specific revisions
  • #! /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
    
    
    
    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")
    
    Steffen Müthing's avatar
    Steffen Müthing committed
                            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))
    
    
    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()