Skip to content

Commit b785ab0

Browse files
alex-meseldzija-sonarsourcecostin-zaharia-sonarsource
authored and
sonartech
committed
NET-767 Add test importer for MSTest format
Co-authored-by: Costin Zaharia <[email protected]>
1 parent ae1b97f commit b785ab0

12 files changed

+748
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* SonarSource :: .NET :: Core
3+
* Copyright (C) 2014-2024 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.plugins.dotnet.tests;
18+
19+
import java.io.File;
20+
import java.util.Map;
21+
import java.util.function.BiConsumer;
22+
23+
@FunctionalInterface
24+
interface UnitTestResultParser extends BiConsumer<File, Map<String, UnitTestResults>> {
25+
}

sonar-dotnet-core/src/main/java/org/sonar/plugins/dotnet/tests/UnitTestResults.java

+18-5
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@
2121

2222
public class UnitTestResults {
2323

24-
private int tests;
25-
private int skipped;
26-
private int failures;
27-
private int errors;
28-
private Long executionTime;
24+
protected int tests;
25+
protected int skipped;
26+
protected int failures;
27+
protected int errors;
28+
protected Long executionTime;
2929

3030
public void add(int tests, int skipped, int failures, int errors, @Nullable Long executionTime) {
3131
this.tests += tests;
@@ -41,6 +41,19 @@ public void add(int tests, int skipped, int failures, int errors, @Nullable Long
4141
}
4242
}
4343

44+
public void add(UnitTestResults unitTestResults) {
45+
this.tests += unitTestResults.tests();
46+
this.skipped += unitTestResults.skipped();
47+
this.failures += unitTestResults.failures();
48+
this.errors += unitTestResults.errors();
49+
if (unitTestResults.executionTime() != null) {
50+
if (this.executionTime == null) {
51+
this.executionTime = 0L;
52+
}
53+
this.executionTime += unitTestResults.executionTime();
54+
}
55+
}
56+
4457
public int tests() {
4558
return tests;
4659
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*
2+
* SonarSource :: .NET :: Core
3+
* Copyright (C) 2014-2024 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.plugins.dotnet.tests;
18+
19+
import java.io.File;
20+
import java.io.IOException;
21+
import java.text.DateFormat;
22+
import java.text.ParseException;
23+
import java.text.SimpleDateFormat;
24+
import java.util.Date;
25+
import java.util.HashMap;
26+
import java.util.Map;
27+
import java.util.regex.Matcher;
28+
import java.util.regex.Pattern;
29+
import org.slf4j.Logger;
30+
import org.slf4j.LoggerFactory;
31+
32+
public class VisualStudioTestResultParser implements UnitTestResultParser {
33+
34+
private static final Logger LOG = LoggerFactory.getLogger(VisualStudioTestResultParser.class);
35+
private final Map<String, String> methodFileMap;
36+
37+
VisualStudioTestResultParser(Map<String, String> methodFileMap) {
38+
this.methodFileMap = methodFileMap;
39+
}
40+
41+
@Override
42+
public void accept(File file, Map<String, UnitTestResults> unitTestResults) {
43+
LOG.info("Parsing the Visual Studio Test Results file '{}'.", file.getAbsolutePath());
44+
new Parser(file, unitTestResults, this.methodFileMap).parse();
45+
}
46+
47+
private static class Parser {
48+
private final File file;
49+
private final Map<String, UnitTestResults> testIdTestResultMap;
50+
private final Map<String, UnitTestResults> unitTestResults;
51+
// Date Format: // https://github.com/microsoft/vstest/blob/7d34b30433259fb914aaaf276fde663a47b6ef2f/src/Microsoft.TestPlatform.Extensions.TrxLogger/XML/XmlPersistence.cs#L557-L572
52+
private final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
53+
private final Pattern millisecondsPattern = Pattern.compile("(\\.(\\d{0,3}))\\d*+");
54+
private final Map<String, String> methodFileMap;
55+
56+
Parser(File file, Map<String, UnitTestResults> unitTestResults, Map<String, String> methodFileMap) {
57+
this.file = file;
58+
this.unitTestResults = unitTestResults;
59+
this.testIdTestResultMap = new HashMap<>();
60+
this.methodFileMap = methodFileMap;
61+
}
62+
63+
public void parse() {
64+
try (XmlParserHelper xmlParserHelper = new XmlParserHelper(file)) {
65+
checkRootTag(xmlParserHelper);
66+
dispatchTags(xmlParserHelper);
67+
} catch (IOException e) {
68+
throw new IllegalStateException("Unable to close report", e);
69+
}
70+
}
71+
72+
private void dispatchTags(XmlParserHelper xmlParserHelper) {
73+
String tagName;
74+
while ((tagName = xmlParserHelper.nextStartTag()) != null) {
75+
if ("UnitTestResult".equals(tagName)) {
76+
handleUnitTestResultTag(xmlParserHelper);
77+
} else if ("UnitTest".equals(tagName)) {
78+
handleUnitTestTag(xmlParserHelper);
79+
}
80+
}
81+
}
82+
83+
private void handleUnitTestResultTag(XmlParserHelper xmlParserHelper) {
84+
String testId = xmlParserHelper.getRequiredAttribute("testId");
85+
String outcome = xmlParserHelper.getRequiredAttribute("outcome");
86+
Date start = getRequiredDateAttribute(xmlParserHelper, "startTime");
87+
Date finish = getRequiredDateAttribute(xmlParserHelper, "endTime");
88+
long duration = finish.getTime() - start.getTime();
89+
90+
var testResult = new VisualStudioTestResults(outcome, duration);
91+
testIdTestResultMap.put(testId, testResult);
92+
LOG.debug("Parsed Visual Studio Unit Test - testId: {} outcome: {}, startTime: {}, endTime: {}",
93+
testId, outcome, start, finish);
94+
}
95+
96+
private void handleUnitTestTag(XmlParserHelper xmlParserHelper) {
97+
String testId = xmlParserHelper.getRequiredAttribute("id");
98+
99+
String tagName;
100+
while ((tagName = xmlParserHelper.nextStartTag()) != null) {
101+
if ("TestMethod".equals(tagName)) {
102+
break;
103+
}
104+
}
105+
if (tagName == null){
106+
throw new ParseErrorException("No TestMethod attribute found on UnitTest tag");
107+
}
108+
109+
String methodName = xmlParserHelper.getRequiredAttribute("name");
110+
String className = xmlParserHelper.getRequiredAttribute("className");
111+
String codeBase = xmlParserHelper.getRequiredAttribute("codeBase");
112+
113+
String dllName = codeBase.substring(codeBase.lastIndexOf(File.separator) + 1, codeBase.lastIndexOf('.'));
114+
115+
String fullyQualifiedName = dllName + "." + className + "." + methodName;
116+
117+
associateTestIdTestResultWithFile(testId, fullyQualifiedName);
118+
}
119+
120+
public void associateTestIdTestResultWithFile(String testId, String methodFullName) {
121+
if(!methodFileMap.containsKey(methodFullName)) {
122+
throw new IllegalStateException(String.format("Test method %s with testId %s cannot be mapped to the test source file", methodFullName, testId));
123+
}
124+
125+
String fileName = methodFileMap.get(methodFullName);
126+
127+
if (unitTestResults.containsKey(fileName)) {
128+
var fileTestResult = unitTestResults.get(fileName);
129+
var testIdTestResult = testIdTestResultMap.get(testId);
130+
fileTestResult.add(testIdTestResult);
131+
} else {
132+
unitTestResults.put(fileName, testIdTestResultMap.get(testId));
133+
}
134+
135+
LOG.debug("Associated Visual Studio Unit Test to File - file: {}, TestId: {} MethodFullName: {}",
136+
fileName, testId, methodFullName);
137+
}
138+
139+
private Date getRequiredDateAttribute(XmlParserHelper xmlParserHelper, String name) {
140+
String value = xmlParserHelper.getRequiredAttribute(name);
141+
try {
142+
value = keepOnlyMilliseconds(value);
143+
return dateFormat.parse(value);
144+
} catch (ParseException e) {
145+
throw xmlParserHelper.parseError("Expected a valid date and time instead of \"" + value + "\" for the attribute \"" + name + "\". " + e.getMessage());
146+
}
147+
}
148+
149+
private String keepOnlyMilliseconds(String value) {
150+
StringBuilder sb = new StringBuilder();
151+
152+
Matcher matcher = millisecondsPattern.matcher(value);
153+
StringBuilder trailingZeros = new StringBuilder();
154+
while (matcher.find()) {
155+
String milliseconds = matcher.group(2);
156+
trailingZeros.setLength(0);
157+
trailingZeros.append("0".repeat(Math.max(0, 3 - milliseconds.length())));
158+
matcher.appendReplacement(sb, "$1" + trailingZeros);
159+
}
160+
matcher.appendTail(sb);
161+
162+
return sb.toString();
163+
}
164+
165+
private static void checkRootTag(XmlParserHelper xmlParserHelper) {
166+
xmlParserHelper.checkRootTag("TestRun");
167+
}
168+
}
169+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* SonarSource :: .NET :: Core
3+
* Copyright (C) 2014-2024 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.plugins.dotnet.tests;
18+
19+
public class VisualStudioTestResults extends UnitTestResults {
20+
21+
VisualStudioTestResults(String outcome, Long executionTime) {
22+
this.tests = 1;
23+
this.executionTime = executionTime;
24+
switch(outcome) {
25+
case "Passed",
26+
"Warning":
27+
// success
28+
break;
29+
case "Failed":
30+
// failure
31+
this.failures = 1;
32+
break;
33+
case "Error":
34+
// error
35+
this.errors = 1;
36+
break;
37+
case "PassedButRunAborted",
38+
"NotExecuted",
39+
"Inconclusive",
40+
"Completed",
41+
"Timeout",
42+
"Aborted",
43+
"Blocked",
44+
"NotRunnable":
45+
//skipped
46+
this.skipped = 1;
47+
break;
48+
default:
49+
throw new IllegalArgumentException("Outcome of unit test must match VSTest Format");
50+
}
51+
}
52+
}

0 commit comments

Comments
 (0)