Skip to content

Commit e52ead4

Browse files
authored
[SUREFIRE-523] Link all reported tests to source XRef (#3445)
Link every reported test name to its test source class while preserving exact source-line links in failure details. Normalize nested classes and report-name suffixes to the corresponding JXR source page, and cover enabled and disabled XRef configurations. Fixes #1162
1 parent 45102fa commit e52ead4

5 files changed

Lines changed: 93 additions & 19 deletions

File tree

maven-surefire-report-plugin/src/main/java/org/apache/maven/plugins/surefire/report/AbstractSurefireReport.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ public abstract class AbstractSurefireReport extends AbstractMavenReport {
7474
private File reportsDirectory;
7575

7676
/**
77-
* Link the violation line numbers to the (Test) Source XRef. Links will be created automatically if the JXR plugin is
78-
* being used.
77+
* Link test names to the (Test) Source XRef. Failure details link to the exact source line when available. Links will
78+
* be created automatically if the JXR plugin is being used.
7979
*/
8080
@Parameter(property = "linkXRef", defaultValue = "true")
8181
private boolean linkXRef;

maven-surefire-report-plugin/src/main/java/org/apache/maven/plugins/surefire/report/SurefireReportRenderer.java

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -366,9 +366,7 @@ private void constructTestCaseSection(ReportTestCase testCase) {
366366

367367
if (!testCase.isSuccessful()) {
368368
sink.tableCell();
369-
sinkAnchor("TC_" + toHtmlId(testCase.getFullName()));
370-
371-
link("#" + toHtmlId(testCase.getFullName()), testCase.getName());
369+
linkTestCaseToSource(testCase);
372370

373371
SinkEventAttributeSet atts = new SinkEventAttributeSet();
374372
atts.addAttribute(CLASS, "detailToggle");
@@ -398,7 +396,9 @@ private void constructTestCaseSection(ReportTestCase testCase) {
398396

399397
sink.tableCell_();
400398
} else {
401-
sinkCellAnchor(testCase.getName(), "TC_" + toHtmlId(testCase.getFullName()));
399+
sink.tableCell();
400+
linkTestCaseToSource(testCase);
401+
sink.tableCell_();
402402
}
403403

404404
tableCell(formatI18nString("surefire", "value.time", testCase.getTime()));
@@ -507,13 +507,13 @@ private void renderSectionFailureDetails() {
507507

508508
String fullClassName = testCase.getFullClassName();
509509
String errorLineNumber = testCase.getFailureErrorLine();
510-
if (xrefTestLocation != null) {
511-
String path = fullClassName.replace('.', '/');
512-
sink.link(xrefTestLocation + "/" + path + ".html#L" + errorLineNumber);
510+
String xrefLocation = getXrefTestSourceLocation(fullClassName, errorLineNumber);
511+
if (xrefLocation != null) {
512+
sink.link(xrefLocation);
513513
}
514514
sink.text(fullClassName + ":" + errorLineNumber);
515515

516-
if (xrefTestLocation != null) {
516+
if (xrefLocation != null) {
517517
sink.link_();
518518
}
519519
sink.unknown("div", TAG_TYPE_END, null);
@@ -531,6 +531,44 @@ private void renderSectionFailureDetails() {
531531
endSection();
532532
}
533533

534+
private void linkTestCaseToSource(ReportTestCase testCase) {
535+
String xrefLocation = getXrefTestSourceLocation(testCase.getFullClassName(), null);
536+
SinkEventAttributeSet atts = new SinkEventAttributeSet(ID, "TC_" + toHtmlId(testCase.getFullName()));
537+
if (xrefLocation == null && !testCase.isSuccessful()) {
538+
xrefLocation = "#" + toHtmlId(testCase.getFullName());
539+
}
540+
if (xrefLocation != null) {
541+
atts.addAttribute(HREF, xrefLocation);
542+
}
543+
544+
sink.unknown(A.toString(), TAG_TYPE_START, atts);
545+
text(testCase.getName());
546+
sink.unknown(A.toString(), TAG_TYPE_END, null);
547+
}
548+
549+
private String getXrefTestSourceLocation(String fullClassName, String lineNumber) {
550+
if (xrefTestLocation == null || fullClassName == null) {
551+
return null;
552+
}
553+
554+
String location = xrefTestLocation + "/" + toXrefTestSourcePath(fullClassName);
555+
return lineNumber == null || lineNumber.isEmpty() ? location : location + "#L" + lineNumber;
556+
}
557+
558+
static String toXrefTestSourcePath(String fullClassName) {
559+
int reportNameSuffix = fullClassName.indexOf('(');
560+
if (reportNameSuffix >= 0) {
561+
fullClassName = fullClassName.substring(0, reportNameSuffix);
562+
}
563+
564+
int nestedClass = fullClassName.indexOf('$');
565+
if (nestedClass >= 0) {
566+
fullClassName = fullClassName.substring(0, nestedClass);
567+
}
568+
569+
return fullClassName.replace('.', '/') + ".html";
570+
}
571+
534572
private void constructHotLinks() {
535573
if (!testSuites.isEmpty()) {
536574
sink.paragraph();

maven-surefire-report-plugin/src/site/markdown/examples/cross-referencing.md.vm

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ date: July 2006
2020
<!-- limitations under the License.-->
2121
# Source Code Cross Reference
2222

23-
There are times when we wish to know right away the line number of the source code that caused a test to the fail. The Surefire Report Plugin has the capability to cross reference the source code that made the test fail. In order to activate this feature, the `maven-jxr-plugin` should be declared in the `<reporting>` section of the POM along with the `maven-surefire-report-plugin`. For more details, please read the documentation of the [Maven JXR Plugin](http://maven.apache.org/plugins/maven-jxr-plugin/).
23+
The Surefire Report Plugin can link every reported test to its source class. For failed tests, the failure details link directly to the source line when it is available from the stack trace. To activate this feature, declare the `maven-jxr-plugin` in the `<reporting>` section of the POM along with the `maven-surefire-report-plugin`. For more details, read the documentation of the [Maven JXR Plugin](https://maven.apache.org/jxr/maven-jxr-plugin/).
2424

2525
```xml
2626
<project>
@@ -46,7 +46,7 @@ There are times when we wish to know right away the line number of the source co
4646
</project>
4747
```
4848

49-
After executing `mvn site` for site generation, you'll notice that from the **Failure Details** section of the report, a link is available to redirect you to the source code that caused the failure.
49+
After executing `mvn site`, each test name links to its source class. The **Failure Details** section additionally links to the source line that caused the failure when that line can be determined.
5050

5151
In the figure below the code that caused the failure is _com.test.proj.AppTest:36_
5252

maven-surefire-report-plugin/src/test/java/org/apache/maven/plugins/surefire/report/SurefireReportTest.java

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,21 @@ void testBasicSurefireReport(SurefireReport mojo) throws Exception {
7777

7878
int idx = htmlContent.indexOf("images/icon_success_sml.gif");
7979
assertTrue(idx >= 0, "Wrong content in file: " + report);
80+
81+
assertThat(
82+
htmlContent,
83+
containsString("<a id=\"TC_com.shape.CircleTest.testX\" "
84+
+ "href=\"./xref-test/com/shape/CircleTest.html\">testX</a>"));
85+
assertThat(
86+
htmlContent,
87+
containsString("<a id=\"TC_com.shape.CircleTest.testRadius\" "
88+
+ "href=\"./xref-test/com/shape/CircleTest.html\">testRadius</a>"));
89+
assertThat(
90+
htmlContent,
91+
containsString("<a href=\"#com.shape.CircleTest.testRadius\">"
92+
+ "<img src=\"images/icon_warning_sml.gif\" /></a>"));
93+
assertThat(htmlContent, containsString("toggleDisplay('com.shape.CircleTest.testRadius')"));
94+
assertThat(htmlContent, containsString("./xref-test/com/shape/CircleTest.html#L34"));
8095
}
8196

8297
@Test
@@ -106,8 +121,7 @@ void testBasicSurefireReportIfLinkXrefIsFalse(SurefireReport mojo) throws Except
106121

107122
String htmlContent = String.join(System.lineSeparator(), Files.readAllLines(report.toPath()));
108123

109-
int idx = htmlContent.indexOf("./xref-test/com/shape/CircleTest.html#L44");
110-
assertEquals(-1, idx);
124+
assertFalse(htmlContent.contains("./xref-test/com/shape/CircleTest.html"));
111125
}
112126

113127
@Test
@@ -119,8 +133,17 @@ void testBasicSurefireReportIfReportingIsNull(SurefireReport mojo) throws Except
119133
assertTrue(report.exists());
120134
String htmlContent = String.join(System.lineSeparator(), Files.readAllLines(report.toPath()));
121135

122-
int idx = htmlContent.indexOf("./xref-test/com/shape/CircleTest.html#L44");
123-
assertTrue(idx < 0);
136+
assertFalse(htmlContent.contains("./xref-test/com/shape/CircleTest.html"));
137+
}
138+
139+
@Test
140+
void testXrefTestSourcePath() {
141+
assertEquals("com/shape/CircleTest.html", SurefireReportRenderer.toXrefTestSourcePath("com.shape.CircleTest"));
142+
assertEquals(
143+
"com/shape/OuterTest.html", SurefireReportRenderer.toXrefTestSourcePath("com.shape.OuterTest$Nested"));
144+
assertEquals(
145+
"com/shape/CircleTest.html",
146+
SurefireReportRenderer.toXrefTestSourcePath("com.shape.CircleTest(Linux)"));
124147
}
125148

126149
@SuppressWarnings("checkstyle:methodname")
@@ -133,11 +156,11 @@ void testBasicSurefireReport_AnchorTestCases(SurefireReport mojo) throws Excepti
133156
assertTrue(report.exists());
134157
String htmlContent = String.join(System.lineSeparator(), Files.readAllLines(report.toPath()));
135158

136-
int idx = htmlContent.indexOf("<td><a id=\"TC_com.shape.CircleTest.testX\"></a>testX</td>");
159+
int idx = htmlContent.indexOf("<td><a id=\"TC_com.shape.CircleTest.testX\">testX</a></td>");
137160
assertTrue(idx > 0);
138161

139-
idx = htmlContent.indexOf("<td><a id=\"TC_com.shape.CircleTest.testRadius\"></a>"
140-
+ "<a href=\"#com.shape.CircleTest.testRadius\">testRadius</a>");
162+
idx = htmlContent.indexOf("<td><a id=\"TC_com.shape.CircleTest.testRadius\" "
163+
+ "href=\"#com.shape.CircleTest.testRadius\">testRadius</a>");
141164
assertTrue(idx > 0);
142165
}
143166

maven-surefire-report-plugin/src/test/java/org/apache/maven/plugins/surefire/report/stubs/ReportTestStub.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,21 @@
1818
*/
1919
package org.apache.maven.plugins.surefire.report.stubs;
2020

21+
import java.io.InputStream;
22+
23+
import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
24+
2125
public class ReportTestStub extends SurefireReportMavenProjectStub {
2226

27+
public ReportTestStub() {
28+
try (InputStream is =
29+
ReportTestStub.class.getResourceAsStream("/unit/basic-surefire-report-test/plugin-config.xml")) {
30+
setModel(new MavenXpp3Reader().read(is));
31+
} catch (Exception e) {
32+
throw new IllegalStateException("Could not read test project model", e);
33+
}
34+
}
35+
2336
@Override
2437
protected String getProjectDirName() {
2538
return "basic-surefire-report-test";

0 commit comments

Comments
 (0)