/* * Sonar C# Plugin :: Gallio * Copyright (C) 2010 Jose Chillan, Alexandre Victoor and SonarSource * dev@sonar.codehaus.org * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 */ package org.sonar.plugins.csharp.gallio.results.execution; import static org.sonar.plugins.csharp.gallio.helper.StaxHelper.advanceCursor; import static org.sonar.plugins.csharp.gallio.helper.StaxHelper.descendantElements; import static org.sonar.plugins.csharp.gallio.helper.StaxHelper.descendantSpecifiedElements; import static org.sonar.plugins.csharp.gallio.helper.StaxHelper.findAttributeValue; import static org.sonar.plugins.csharp.gallio.helper.StaxHelper.findElementName; import static org.sonar.plugins.csharp.gallio.helper.StaxHelper.isAStartElement; import static org.sonar.plugins.csharp.gallio.helper.StaxHelper.nextPosition; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.xml.namespace.QName; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; import org.apache.commons.lang.StringUtils; import org.codehaus.staxmate.SMInputFactory; import org.codehaus.staxmate.in.SMEvent; import org.codehaus.staxmate.in.SMFilterFactory; import org.codehaus.staxmate.in.SMHierarchicCursor; import org.codehaus.staxmate.in.SMInputCursor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.utils.SonarException; import org.sonar.plugins.csharp.gallio.results.execution.model.TestCaseDetail; import org.sonar.plugins.csharp.gallio.results.execution.model.TestDescription; import org.sonar.plugins.csharp.gallio.results.execution.model.TestStatus; import org.sonar.plugins.csharp.gallio.results.execution.model.UnitTestReport; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; /** * Gallio result report parser. * * @author Maxime SCHNEIDER-DUFEUTRELLE * */ public class GallioResultParser { private static final String LOG_PATTERN = "--{} : {}"; private static final String GALLIO_REPORT_PARSING_ERROR = "gallio report parsing error"; private static final String GALLIO_URI = "http://www.gallio.org/"; private static final String ASSEMBLY = "assembly"; private static final String NAMESPACE = "namespace"; private static final String TYPE = "type"; private static final String MEMBER = "member"; private static final String PATH = "path"; private static final String LINE = "line"; private static final Logger LOG = LoggerFactory.getLogger(GallioResultParser.class); private Map<String, TestCaseDetail> testCaseDetailsByTestIds; public Set<UnitTestReport> parse(File report) { try { testCaseDetailsByTestIds = new HashMap<String, TestCaseDetail>(); SMInputFactory inf = new SMInputFactory(XMLInputFactory.newInstance()); SMHierarchicCursor rootCursor = inf.rootElementCursor(report); advanceCursor(rootCursor); LOG.debug("rootCursor is at : {}", findElementName(rootCursor)); // We first get the tests ids and put them in a map to get the details later Map<String, TestDescription> testsDetails = new HashMap<String, TestDescription>(); QName testModelTag = new QName(GALLIO_URI, "testModel"); SMInputCursor testModelCursor = descendantElements(rootCursor); testModelCursor.setFilter(SMFilterFactory.getElementOnlyFilter(testModelTag)); advanceCursor(testModelCursor); LOG.debug("TestModelCursor initialized at : {}", findElementName(testModelCursor)); testsDetails = recursiveParseTestsIds(testModelCursor, testsDetails, null, null); QName testPackageRunTag = new QName(GALLIO_URI, "testPackageRun"); testModelCursor.setFilter(SMFilterFactory.getElementOnlyFilter(testPackageRunTag)); advanceCursor(testModelCursor); String testId = ""; recursiveParseTestsResults(testModelCursor, testId); // Finally, we fill the reports final Set<UnitTestReport> reports = createUnitTestsReport(testsDetails); rootCursor.getStreamReader().closeCompletely(); LOG.debug("Parsing ended"); return reports; } catch (XMLStreamException e) { throw new SonarException(GALLIO_REPORT_PARSING_ERROR, e); } } private Map<String, TestDescription> recursiveParseTestsIds(SMInputCursor rootCursor, Map<String, TestDescription> testDetails, File source, String parentAssemblyName) { File sourceFile = source; QName testTag = new QName(GALLIO_URI, "test"); if (isAStartElement(rootCursor)) { // Get all the tests SMInputCursor currentTest = descendantSpecifiedElements(rootCursor, testTag); while (null != nextPosition(currentTest) && isAStartElement(currentTest)) { TestDescription testDescription = new TestDescription(); String id = findAttributeValue(currentTest, "id"); String name = findAttributeValue(currentTest, "name"); String testCase = findAttributeValue(currentTest, "isTestCase"); LOG.debug("Id : {} & isTestCase : {}", id, testCase); boolean isTestCase = "true".equals(testCase); // We analyse all the tests tags to get usefull informations if the test is a TestCase, // and to get their children SMInputCursor currentTestChildren = descendantElements(currentTest); String eltName = null; while (null != nextPosition(currentTestChildren) && !"parameters".equals(eltName)) { eltName = findElementName(currentTestChildren); if (isTestCase) { testDescription.setMethodName(name); LOG.debug(eltName); if ("codeReference".equals(eltName)) { parentAssemblyName = codeReferenceTreatment(parentAssemblyName, testDescription, currentTestChildren); retrieveCodeReferences(testDescription, currentTestChildren); } nextPosition(currentTestChildren); if ("codeLocation".equals(eltName)) { sourceFile = retrieveCodeLocation(sourceFile, testDescription, currentTestChildren); } if (null == testDescription.getSourceFile()) { testDescription.setSourceFile(sourceFile); } testDetails.put(id, testDescription); } sourceFile = evaluatePath(sourceFile, eltName, currentTestChildren); if ("children".equals(eltName)) { recursiveParseTestsIds(currentTestChildren, testDetails, sourceFile, parentAssemblyName); } advanceCursor(currentTestChildren); } } } return testDetails; } private String codeReferenceTreatment(String parentAssemblyName, TestDescription testDescription, SMInputCursor currentTestChildren) { String assemblyName = parentAssemblyName; String attributeValue; if (null != findAttributeValue(currentTestChildren, ASSEMBLY)) { attributeValue = findAttributeValue(currentTestChildren, ASSEMBLY); LOG.debug(LOG_PATTERN, ASSEMBLY, attributeValue); testDescription.setAssemblyName(StringUtils.substringBefore(attributeValue, ",")); assemblyName = testDescription.getAssemblyName(); } else { // Get the precedent assemblyName if not filled testDescription.setAssemblyName(assemblyName); } return assemblyName; } private File evaluatePath(File source, String eltName, SMInputCursor currentTestChildren) { File sourceFile = source; if ("codeLocation".equals(eltName) && null != findAttributeValue(currentTestChildren, PATH)) { File currentSourceFile = new File(findAttributeValue(currentTestChildren, PATH)); if (currentSourceFile != null) { sourceFile = currentSourceFile; } } return sourceFile; } private void retrieveCodeReferences(TestDescription testDescription, SMInputCursor currentTestChildren) { String attributeValue; if (null != findAttributeValue(currentTestChildren, NAMESPACE)) { attributeValue = findAttributeValue(currentTestChildren, NAMESPACE); LOG.debug(LOG_PATTERN, NAMESPACE, attributeValue); testDescription.setNamespace(attributeValue); } if (null != findAttributeValue(currentTestChildren, TYPE)) { attributeValue = findAttributeValue(currentTestChildren, TYPE); LOG.debug(LOG_PATTERN, TYPE, attributeValue); testDescription.setClassName(attributeValue); } if (null != findAttributeValue(currentTestChildren, MEMBER)) { attributeValue = findAttributeValue(currentTestChildren, MEMBER); LOG.debug(LOG_PATTERN, MEMBER, attributeValue); testDescription.setMethodName(attributeValue); } } private File retrieveCodeLocation(File source, TestDescription testDescription, SMInputCursor currentTestChildren) { File sourceFile = source; String attributeValue; if (null != findAttributeValue(currentTestChildren, PATH)) { attributeValue = findAttributeValue(currentTestChildren, PATH); LOG.debug(LOG_PATTERN, PATH, attributeValue); File currentSourceFile = new File(attributeValue); testDescription.setSourceFile(currentSourceFile); sourceFile = currentSourceFile; } if (null != findAttributeValue(currentTestChildren, LINE)) { attributeValue = findAttributeValue(currentTestChildren, LINE); LOG.debug(LOG_PATTERN, LINE, attributeValue); int lineNumber = Integer.valueOf(attributeValue); testDescription.setLine(lineNumber); } return sourceFile; } private void recursiveParseTestsResults(SMInputCursor rootCursor, String testId) { String currentTestId = testId; QName testStepRunTag = new QName(GALLIO_URI, "testStepRun"); SMInputCursor currentTestStepRun = descendantSpecifiedElements(rootCursor, testStepRunTag); String eltName = ""; while (null != nextPosition(currentTestStepRun) && isAStartElement(currentTestStepRun)) { // currentTestTags represents the different tests SMInputCursor currentTestTags = descendantElements(currentTestStepRun); nextPosition(currentTestTags); eltName = findElementName(currentTestTags); if ("testStep".equals(eltName)) { if ("true".equals(findAttributeValue(currentTestTags, "isTestCase"))) { if (null != findAttributeValue(currentTestTags, "testId")) { currentTestId = findAttributeValue(currentTestTags, "testId"); LOG.debug("--testId : {}", currentTestId); LOG.debug("--isTestCase : {}", findAttributeValue(currentTestTags, "isTestCase")); nextPosition(currentTestTags); } while (null != nextPosition(currentTestTags)) { TestCaseDetail testCaseDetail = parsingTags(currentTestTags, currentTestId); if (null != testCaseDetail) { testCaseDetailsByTestIds.put(currentTestId, testCaseDetail); } } } else { currentTestId = findAttributeValue(currentTestTags, "testId"); while (null != nextPosition(currentTestTags)) { parseChildren(currentTestId, currentTestTags); } } } advanceCursor(currentTestTags); } } private void parseChildren(String testId, SMInputCursor currentTestTags) { if ("children".equals(findElementName(currentTestTags))) { recursiveParseTestsResults(currentTestTags, testId); nextPosition(currentTestTags); } } private TestCaseDetail parsingTags(SMInputCursor currentTestTags, String testId) { parseChildren(testId, currentTestTags); TestCaseDetail detail = new TestCaseDetail(); if ("result".equals(findElementName(currentTestTags))) { LOG.debug("Result for test : {}", testId); String assertCount = findAttributeValue(currentTestTags, "assertCount"); LOG.debug("---assertCount : {}", assertCount); detail.setCountAsserts((int) Double.parseDouble(assertCount)); String duration = findAttributeValue(currentTestTags, "duration"); LOG.debug("---duration : {}", duration); detail.setTimeMillis((int) Math.round(Double.parseDouble(duration) * 1000.)); SMInputCursor currentTestOutcomeResultCursor = descendantElements(currentTestTags); advanceCursor(currentTestOutcomeResultCursor); String status = findAttributeValue(currentTestOutcomeResultCursor, "status"); String category = null; if (null != findAttributeValue(currentTestOutcomeResultCursor, "category")) { category = findAttributeValue(currentTestOutcomeResultCursor, "category"); } LOG.debug("---status : {}", status); TestStatus executionStatus = TestStatus.computeStatus(status, category); nextPosition(currentTestTags); detail.setStatus(executionStatus); if ((executionStatus == TestStatus.FAILED) || (executionStatus == TestStatus.ERROR)) { detail = getMessages(currentTestTags, detail); } return detail; } return null; } private TestCaseDetail getMessages(SMInputCursor currentTestTags, TestCaseDetail detail) { if ("testLog".equals(findElementName(currentTestTags))) { SMInputCursor currentTestLogStreamsTags = descendantElements(currentTestTags); SMEvent streamsTag = nextPosition(currentTestLogStreamsTags); if (null != streamsTag) { LOG.debug("----streams Tag found : {}", findElementName(currentTestLogStreamsTags)); if (streamsTag.getEventCode() == SMEvent.START_ELEMENT.getEventCode()) { LOG.debug("----Cursor is at <streams> Tag "); SMInputCursor currentTestLogStreamTags = descendantElements(currentTestLogStreamsTags); parseStreams(detail, currentTestLogStreamTags); } } } return detail; } private void parseStreams(TestCaseDetail detail, SMInputCursor currentTestLogStreamTags) { try { while (null != nextPosition(currentTestLogStreamTags)) { LOG.debug("----Cursor is at <stream> Tag "); String streamName = findAttributeValue(currentTestLogStreamTags, "name"); LOG.debug("----stream name : {}", streamName); SMInputCursor currentTestLogStreamSectionsTags = currentTestLogStreamTags.descendantElementCursor().advance() .descendantElementCursor().advance().descendantElementCursor(); while (null != nextPosition(currentTestLogStreamSectionsTags)) { SMInputCursor sectionContentsChild = currentTestLogStreamSectionsTags; if ("section".equals(findElementName(currentTestLogStreamSectionsTags))) { String sectionName = findAttributeValue(currentTestLogStreamSectionsTags, "name"); LOG.debug("----section name : {}", sectionName); sectionContentsChild = currentTestLogStreamSectionsTags.descendantElementCursor().advance().descendantElementCursor().advance(); } if ("text".equals(findElementName(sectionContentsChild))) { String message = sectionContentsChild.collectDescendantText(); LOG.debug("Error Message is : {}", message); detail.setErrorMessage(message); } else if ("marker".equals(findElementName(sectionContentsChild)) && isAStartElement(sectionContentsChild)) { LOG.debug("-------Marker found ! "); if ("StackTrace".equals(findAttributeValue(sectionContentsChild, "class"))) { SMInputCursor sectionMarkerTextContent = sectionContentsChild.descendantElementCursor().advance().descendantElementCursor() .advance(); String stackTrace = sectionMarkerTextContent.collectDescendantText(); LOG.debug("StackTrace is : {}", stackTrace); detail.setStackTrace(stackTrace); } } } } } catch (XMLStreamException e) { LOG.error(GALLIO_REPORT_PARSING_ERROR, e); } } private Set<UnitTestReport> createUnitTestsReport(Map<String, TestDescription> testsDescriptionByTestIds) { Set<UnitTestReport> result = new HashSet<UnitTestReport>(); Set<String> testIds = testCaseDetailsByTestIds.keySet(); // We associate the descriptions with the test details List<String> testsToRemove = new ArrayList<String>(); for (String testId : testIds) { TestDescription description = testsDescriptionByTestIds.get(testId); TestCaseDetail testCaseDetail = testCaseDetailsByTestIds.get(testId); if (description == null) { LOG.debug( "Test {} is not considered as a testCase in your xml, there should not be any testStep associated, please check your gallio report. Skipping result", testId); testsToRemove.add(testId); } else { testCaseDetail.merge(description); testCaseDetailsByTestIds.put(testId, testCaseDetail); } } LOG.debug("Tests to be removed {}", testsToRemove.size()); for (String testToRemove : testsToRemove) { testCaseDetailsByTestIds.remove(testToRemove); } Collection<TestCaseDetail> testCases = testCaseDetailsByTestIds.values(); Multimap<String, TestCaseDetail> testCaseDetailsBySrcKey = ArrayListMultimap.create(); for (TestCaseDetail testCaseDetail : testCases) { String sourceKey = testCaseDetail.createSourceKey(); testCaseDetailsBySrcKey.put(sourceKey, testCaseDetail); } Map<String, UnitTestReport> unitTestsReports = new HashMap<String, UnitTestReport>(); LOG.debug("testCaseDetails size : {}", String.valueOf(testCaseDetailsByTestIds.size())); Set<String> pathKeys = testCaseDetailsBySrcKey.keySet(); LOG.debug("There are {} different pathKeys", String.valueOf(pathKeys.size())); for (String pathKey : pathKeys) { // If the Key already exists in the map, we add the details if (unitTestsReports.containsKey(pathKey)) { UnitTestReport unitTest = unitTestsReports.get(pathKey); for (TestCaseDetail testDetail : testCaseDetailsBySrcKey.get(pathKey)) { LOG.debug("Adding testDetail {} to the unitTestReport", testDetail.getName()); unitTest.addDetail(testDetail); } unitTestsReports.put(pathKey, unitTest); } else { // Else we create a new report UnitTestReport unitTest = new UnitTestReport(); unitTest.setAssemblyName(testCaseDetailsBySrcKey.get(pathKey).iterator().next().getAssemblyName()); unitTest.setSourceFile(testCaseDetailsBySrcKey.get(pathKey).iterator().next().getSourceFile()); LOG.debug("Create new unitTest for path : {}", unitTest.getSourceFile().getPath()); for (TestCaseDetail testDetail : testCaseDetailsBySrcKey.get(pathKey)) { LOG.debug("+ and add details : {}", testDetail.getName()); unitTest.addDetail(testDetail); } unitTestsReports.put(pathKey, unitTest); } } result.addAll(unitTestsReports.values()); LOG.debug("The result Set contains " + result.size() + " report(s)"); return result; } }