|
| 1 | +package fi.helsinki.cs.tmc.langs.qmake; |
| 2 | + |
| 3 | +import fi.helsinki.cs.tmc.langs.domain.RunResult; |
| 4 | +import fi.helsinki.cs.tmc.langs.domain.RunResult.Status; |
| 5 | +import fi.helsinki.cs.tmc.langs.domain.TestResult; |
| 6 | + |
| 7 | +import com.google.common.collect.ImmutableList; |
| 8 | +import com.google.common.collect.ImmutableMap; |
| 9 | + |
| 10 | +import org.slf4j.Logger; |
| 11 | +import org.slf4j.LoggerFactory; |
| 12 | + |
| 13 | +import org.w3c.dom.Document; |
| 14 | +import org.w3c.dom.Element; |
| 15 | +import org.w3c.dom.NodeList; |
| 16 | + |
| 17 | +import org.xml.sax.InputSource; |
| 18 | +import org.xml.sax.SAXException; |
| 19 | + |
| 20 | +import java.io.FileInputStream; |
| 21 | +import java.io.IOException; |
| 22 | +import java.io.InputStream; |
| 23 | +import java.io.InputStreamReader; |
| 24 | +import java.io.Reader; |
| 25 | +import java.nio.file.Path; |
| 26 | +import java.util.ArrayList; |
| 27 | +import java.util.List; |
| 28 | + |
| 29 | +import javax.xml.parsers.DocumentBuilder; |
| 30 | +import javax.xml.parsers.DocumentBuilderFactory; |
| 31 | +import javax.xml.parsers.ParserConfigurationException; |
| 32 | + |
| 33 | +public final class QTestResultParser { |
| 34 | + |
| 35 | + private static final String DOC_NULL_ERROR_MESSAGE = "Failed to parse test results"; |
| 36 | + private static final String SAX_PARSER_ERROR = "SAX parser error occured"; |
| 37 | + private static final String PARSING_DONE_MESSAGE = "Qt test cases parsed."; |
| 38 | + |
| 39 | + private static final Logger log = LoggerFactory.getLogger(QTestResultParser.class); |
| 40 | + |
| 41 | + private final List<TestResult> tests; |
| 42 | + |
| 43 | + public QTestResultParser(Path testResults) { |
| 44 | + this.tests = parseTestCases(testResults); |
| 45 | + } |
| 46 | + |
| 47 | + private List<TestResult> parseTestCases(Path testOutput) { |
| 48 | + Document doc; |
| 49 | + try { |
| 50 | + doc = prepareDocument(testOutput); |
| 51 | + } catch (ParserConfigurationException | IOException e) { |
| 52 | + log.error("Unexpected exception, could not parse Qt testcases.", e); |
| 53 | + return new ArrayList<>(); |
| 54 | + } |
| 55 | + |
| 56 | + NodeList nodeList = doc.getElementsByTagName("TestFunction"); |
| 57 | + List<TestResult> cases = createQtTestResults(nodeList); |
| 58 | + |
| 59 | + log.info(PARSING_DONE_MESSAGE); |
| 60 | + |
| 61 | + return cases; |
| 62 | + } |
| 63 | + |
| 64 | + private Document prepareDocument(Path testOutput) |
| 65 | + throws ParserConfigurationException, IOException { |
| 66 | + DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); |
| 67 | + DocumentBuilder documentBuilder = dbFactory.newDocumentBuilder(); |
| 68 | + documentBuilder.setErrorHandler(null); // Silence logging |
| 69 | + dbFactory.setValidating(false); |
| 70 | + |
| 71 | + InputStream inputStream = new FileInputStream(testOutput.toFile()); |
| 72 | + Reader reader = new InputStreamReader(inputStream, "UTF-8"); |
| 73 | + InputSource is = new InputSource(reader); |
| 74 | + is.setEncoding("UTF-8"); |
| 75 | + |
| 76 | + Document doc = null; |
| 77 | + try { |
| 78 | + doc = documentBuilder.parse(is); |
| 79 | + } catch (SAXException ex) { |
| 80 | + log.info(SAX_PARSER_ERROR); |
| 81 | + log.info(ex.toString()); |
| 82 | + } |
| 83 | + |
| 84 | + if (doc == null) { |
| 85 | + log.info(DOC_NULL_ERROR_MESSAGE); |
| 86 | + throw new IllegalStateException(DOC_NULL_ERROR_MESSAGE); |
| 87 | + } |
| 88 | + |
| 89 | + doc.getDocumentElement().normalize(); |
| 90 | + |
| 91 | + return doc; |
| 92 | + } |
| 93 | + |
| 94 | + /** |
| 95 | + * Parses Qt testlib XML output, as generated with -o filename.xml,xml. |
| 96 | + * |
| 97 | + * Points are mapped to test cases with Message node type 'qinfo'. These |
| 98 | + * messages contain: prefix "TMC:" test case name: "test_function_name" |
| 99 | + * points separated by period: ".1" |
| 100 | + * |
| 101 | + * With failing testcase assertions, there will be an Incident node with |
| 102 | + * type "fail" and Description node with the failed assertion message. |
| 103 | + * |
| 104 | + * When the testcase assertion(s) has passed, there will be an Incident node |
| 105 | + * with type "pass" and no Description node. |
| 106 | + * |
| 107 | + * <TestFunction name="test_function_name"> |
| 108 | + * <Message type="qinfo" file="" line="0"> |
| 109 | + * <Description><![CDATA[TMC:test_function_name.1]]></Description> |
| 110 | + * </Message> |
| 111 | + * <Incident type="fail" file="test_source.cpp" line="420"> |
| 112 | + * <Description><![CDATA['!strcmp(hello_msg(), "Helo, world!" )' returned FALSE. ()]]></Description> |
| 113 | + * </Incident> |
| 114 | + * <Duration msecs="0.135260"/> |
| 115 | + * </TestFunction> |
| 116 | + * |
| 117 | + */ |
| 118 | + private List<TestResult> createQtTestResults(NodeList nodeList) { |
| 119 | + List<TestResult> cases = new ArrayList<>(); |
| 120 | + |
| 121 | + for (int i = 0; i < nodeList.getLength(); i++) { |
| 122 | + Element testcase = (Element) nodeList.item(i); |
| 123 | + List<String> points = parsePoints(testcase); |
| 124 | + |
| 125 | + if (points.isEmpty()) { |
| 126 | + // No points == not a TMC testcase |
| 127 | + continue; |
| 128 | + } |
| 129 | + |
| 130 | + Element incident = (Element) testcase.getElementsByTagName("Incident").item(0); |
| 131 | + |
| 132 | + String id = testcase.getAttribute("name"); |
| 133 | + boolean passed = incident.getAttribute("type").equals("pass"); |
| 134 | + String msg = ""; |
| 135 | + |
| 136 | + // Get the assertion error if testcase failed |
| 137 | + if (!passed) { |
| 138 | + Element desc = (Element) incident.getElementsByTagName("Description").item(0); |
| 139 | + msg = desc.getTextContent(); |
| 140 | + } |
| 141 | + |
| 142 | + ImmutableList<String> trace = ImmutableList.of(); |
| 143 | + cases.add(new TestResult(id, passed, ImmutableList.copyOf(points), msg, trace)); |
| 144 | + } |
| 145 | + |
| 146 | + return cases; |
| 147 | + } |
| 148 | + |
| 149 | + /** |
| 150 | + * |
| 151 | + * Parse potential points from testcase. |
| 152 | + * |
| 153 | + */ |
| 154 | + private List<String> parsePoints(Element testcase) { |
| 155 | + List<String> points = new ArrayList<>(); |
| 156 | + NodeList messages = testcase.getElementsByTagName("Message"); |
| 157 | + for (int i = 0; i < messages.getLength(); i++) { |
| 158 | + Element message = (Element) messages.item(i); |
| 159 | + Element desc = (Element) message.getElementsByTagName("Description").item(0); |
| 160 | + String text = desc.getTextContent(); |
| 161 | + if (text.matches("^(TMC:.*)")) { |
| 162 | + String[] split = text.split("\\."); |
| 163 | + points.add(split[1]); |
| 164 | + } |
| 165 | + } |
| 166 | + |
| 167 | + return points; |
| 168 | + } |
| 169 | + |
| 170 | + /** |
| 171 | + * Returns the test results of the tests in this file. |
| 172 | + */ |
| 173 | + public List<TestResult> getTestResults() { |
| 174 | + return this.tests; |
| 175 | + } |
| 176 | + |
| 177 | + /** |
| 178 | + * Returns the combined status of the tests in this file. |
| 179 | + */ |
| 180 | + public Status getResultStatus() { |
| 181 | + for (TestResult result : getTestResults()) { |
| 182 | + if (!result.isSuccessful()) { |
| 183 | + return Status.TESTS_FAILED; |
| 184 | + } |
| 185 | + } |
| 186 | + |
| 187 | + return Status.PASSED; |
| 188 | + } |
| 189 | + |
| 190 | + /** |
| 191 | + * Returns the run result of this file. |
| 192 | + */ |
| 193 | + public RunResult result() { |
| 194 | + return new RunResult( |
| 195 | + getResultStatus(), |
| 196 | + ImmutableList.copyOf(getTestResults()), |
| 197 | + new ImmutableMap.Builder<String, byte[]>().build()); |
| 198 | + } |
| 199 | +} |
0 commit comments