Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,13 +234,14 @@ doubt.

The JUnit XML format exposes each analyzed method as a testcase and is shaped
for GitLab's Tests tab. Testcases use the project-relative source path for
`classname` and `file`, use `method:lineStart` as the testcase `name`, write the
`classname` and `file`, include a concise metric summary in the testcase `name`
as `method:lineStart [CRAP=score, CC=complexity, Cov=percent (kind)]`, write the
measured analysis duration on the testsuite, and divide that duration across
testcases. Methods with CRAP scores over the configured threshold fail, methods
with unavailable coverage are skipped, and failure/skipped element text includes
CRAP score, threshold, coverage kind, source path, and line range. Custom
properties remain for tools that read them, but GitLab-visible details do not
rely on properties.
with unavailable coverage are skipped, and testcase-level `system-out` plus
failure/skipped element text include CRAP score, threshold, coverage kind,
source path, and line range. Custom properties remain for tools that read them,
but GitLab-visible details do not rely on properties.

## Distribution

Expand Down
28 changes: 21 additions & 7 deletions core/src/main/java/media/barney/crap/core/ReportFormatter.java
Original file line number Diff line number Diff line change
Expand Up @@ -263,15 +263,17 @@ private static JunitTestCase junitTestCase(CrapReport.MethodReport method,
double threshold,
boolean omitRedundancy,
String time) {
String diagnosticText = junitDiagnosticText(method, threshold);
return new JunitTestCase(
method.sourcePath(),
testcaseName(method),
method.sourcePath(),
method.startLine(),
time,
junitProperties(method, omitRedundancy),
junitFailure(method, threshold),
junitSkipped(method, threshold)
diagnosticText,
junitFailure(method, threshold, diagnosticText),
junitSkipped(method, diagnosticText)
);
}

Expand Down Expand Up @@ -317,22 +319,24 @@ private static JunitProperties junitProperties(CrapReport.MethodReport method, b
return new JunitProperties(properties);
}

private static @Nullable JunitFailure junitFailure(CrapReport.MethodReport method, double threshold) {
private static @Nullable JunitFailure junitFailure(CrapReport.MethodReport method,
double threshold,
String diagnosticText) {
if (method.status() != MethodStatus.FAILED) {
return null;
}
String message = "CRAP threshold exceeded: "
+ formatDisplayNumber(method.crapScore()) + " > " + formatDisplayNumber(threshold);
return new JunitFailure(message, "crap-java.threshold", junitDiagnosticText(method, threshold));
return new JunitFailure(message, "crap-java.threshold", diagnosticText);
}

private static @Nullable JunitSkipped junitSkipped(CrapReport.MethodReport method, double threshold) {
private static @Nullable JunitSkipped junitSkipped(CrapReport.MethodReport method, String diagnosticText) {
if (method.status() != MethodStatus.SKIPPED) {
return null;
}
return new JunitSkipped(
"CRAP score unavailable",
junitDiagnosticText(method, threshold)
diagnosticText
);
}

Expand All @@ -353,7 +357,16 @@ private static ObjectMapper xmlMapper() {
}

private static String testcaseName(CrapReport.MethodReport method) {
return method.methodName() + ":" + method.startLine();
return String.format(
Locale.ROOT,
"%s:%d [CRAP=%s, CC=%d, Cov=%s (%s)]",
method.methodName(),
method.startLine(),
formatDisplayNumber(method.crapScore()),
method.complexity(),
formatCoverage(method.coveragePercent()),
method.coverageKind()
);
}

private static String junitDiagnosticText(CrapReport.MethodReport method, double threshold) {
Expand Down Expand Up @@ -489,6 +502,7 @@ private record JunitTestCase(
@JacksonXmlProperty(isAttribute = true) int line,
@JacksonXmlProperty(isAttribute = true) String time,
@JacksonXmlProperty(localName = "properties") JunitProperties properties,
@JacksonXmlProperty(localName = "system-out") String systemOut,
@Nullable JunitFailure failure,
@Nullable JunitSkipped skipped
) {
Expand Down
24 changes: 12 additions & 12 deletions core/src/test/java/media/barney/crap/core/MainTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -292,9 +292,9 @@ void noneFormatSuppressesStdoutButKeepsJunitSidecarComplete() throws Exception {
assertEquals(2, exit);
assertEquals("", utf8(out));
assertTrue(junit.contains("<testsuites tests=\"3\" failures=\"1\" errors=\"0\" skipped=\"1\" time=\""));
assertTrue(junit.contains("name=\"danger:4\""));
assertTrue(junit.contains("name=\"safe:14\""));
assertTrue(junit.contains("name=\"unknown:18\""));
assertTrue(junit.contains("name=\"danger:4 [CRAP="));
assertTrue(junit.contains("name=\"safe:14 [CRAP="));
assertTrue(junit.contains("name=\"unknown:18 [CRAP="));
}

@Test
Expand Down Expand Up @@ -357,9 +357,9 @@ void agentModeComposesPrimaryDefaultsButKeepsJunitSidecarComplete() throws Excep
assertFalse(primary.contains("\"method\": \"safe\""));
assertFalse(primary.contains("\"method\": \"unknown\""));
assertTrue(junit.contains("<testsuites tests=\"3\" failures=\"1\" errors=\"0\" skipped=\"1\" time=\""));
assertTrue(junit.contains("name=\"danger:4\""));
assertTrue(junit.contains("name=\"safe:14\""));
assertTrue(junit.contains("name=\"unknown:18\""));
assertTrue(junit.contains("name=\"danger:4 [CRAP="));
assertTrue(junit.contains("name=\"safe:14 [CRAP="));
assertTrue(junit.contains("name=\"unknown:18 [CRAP="));
}

@Test
Expand Down Expand Up @@ -426,9 +426,9 @@ void failuresOnlyFiltersPrimaryOutputButKeepsJunitSidecarComplete() throws Excep
assertFalse(primary.contains("\"method\": \"safe\""));
assertFalse(primary.contains("\"method\": \"unknown\""));
assertTrue(junit.contains("<testsuites tests=\"3\" failures=\"1\" errors=\"0\" skipped=\"1\" time=\""));
assertTrue(junit.contains("name=\"danger:4\""));
assertTrue(junit.contains("name=\"safe:14\""));
assertTrue(junit.contains("name=\"unknown:18\""));
assertTrue(junit.contains("name=\"danger:4 [CRAP="));
assertTrue(junit.contains("name=\"safe:14 [CRAP="));
assertTrue(junit.contains("name=\"unknown:18 [CRAP="));
}

@Test
Expand Down Expand Up @@ -499,9 +499,9 @@ void runWithExistingCoveragePreResolvedModulesHonorsPrimaryReportControls() thro
assertTrue(primary.contains("\"method\": \"danger\""));
assertFalse(primary.contains("\"method\": \"safe\""));
assertFalse(primary.contains("\"method\": \"unknown\""));
assertTrue(junit.contains("name=\"danger:4\""));
assertTrue(junit.contains("name=\"safe:14\""));
assertTrue(junit.contains("name=\"unknown:18\""));
assertTrue(junit.contains("name=\"danger:4 [CRAP="));
assertTrue(junit.contains("name=\"safe:14 [CRAP="));
assertTrue(junit.contains("name=\"unknown:18 [CRAP="));
}

@Test
Expand Down
34 changes: 27 additions & 7 deletions core/src/test/java/media/barney/crap/core/ReportFormatterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ void formatsFailuresOnlyJunitWithOnlyFailedMethods() throws Exception {

Document document = parseXml(report);
Element root = document.getDocumentElement();
Element danger = testcaseByName(document, "danger:4");
Element danger = testcaseByName(document, "danger:4 [CRAP=9.6, CC=5, Cov=10.0% (instruction)]");

assertEquals("1", root.getAttribute("tests"));
assertEquals("1", root.getAttribute("failures"));
Expand All @@ -220,8 +220,8 @@ void formatsFailuresOnlyJunitWithOnlyFailedMethods() throws Exception {
assertEquals("src/main/java/demo/Sample.java", danger.getAttribute("file"));
assertEquals("0.0", danger.getAttribute("time"));
assertTrue(report.contains("<property name=\"threshold\" value=\"8.0\"/>"));
assertFalse(hasTestcase(document, "safe:9"));
assertFalse(hasTestcase(document, "unknown:20"));
assertFalse(hasTestcaseNameStartingWith(document, "safe:9 "));
assertFalse(hasTestcaseNameStartingWith(document, "unknown:20 "));
}

@Test
Expand Down Expand Up @@ -370,8 +370,10 @@ void formatsJunitReportWithFailuresSkippedAndProperties() throws Exception {
Element suite = (Element) document.getElementsByTagName("testsuite").item(0);
Element failure = (Element) document.getElementsByTagName("failure").item(0);
Element skipped = (Element) document.getElementsByTagName("skipped").item(0);
Element danger = testcaseByName(document, "danger:4");
Element unknown = testcaseByName(document, "unknown:20");
Element danger = testcaseByName(document, "danger:4 [CRAP=9.6, CC=5, Cov=10.0% (instruction)]");
Element unknown = testcaseByName(document, "unknown:20 [CRAP=N/A, CC=2, Cov=N/A (N/A)]");
String dangerSystemOut = childText(danger, "system-out");
String unknownSystemOut = childText(unknown, "system-out");

assertEquals("testsuites", root.getNodeName());
assertEquals("2", root.getAttribute("tests"));
Expand All @@ -395,11 +397,19 @@ void formatsJunitReportWithFailuresSkippedAndProperties() throws Exception {
assertTrue(failure.getTextContent().contains("Threshold: 8.0"));
assertTrue(failure.getTextContent().contains("Coverage: 10.0% (instruction)"));
assertTrue(failure.getTextContent().contains("Source: src/main/java/demo/Sample.java:4-6"));
assertTrue(dangerSystemOut.contains("CRAP score: 9.6"));
assertTrue(dangerSystemOut.contains("Threshold: 8.0"));
assertTrue(dangerSystemOut.contains("Coverage: 10.0% (instruction)"));
assertTrue(dangerSystemOut.contains("Source: src/main/java/demo/Sample.java:4-6"));
assertEquals("CRAP score unavailable", skipped.getAttribute("message"));
assertTrue(skipped.getTextContent().contains("CRAP score: N/A"));
assertTrue(skipped.getTextContent().contains("Threshold: 8.0"));
assertTrue(skipped.getTextContent().contains("Coverage: N/A (N/A)"));
assertTrue(skipped.getTextContent().contains("Source: src/main/java/demo/Sample.java:20-22"));
assertTrue(unknownSystemOut.contains("CRAP score: N/A"));
assertTrue(unknownSystemOut.contains("Threshold: 8.0"));
assertTrue(unknownSystemOut.contains("Coverage: N/A (N/A)"));
assertTrue(unknownSystemOut.contains("Source: src/main/java/demo/Sample.java:20-22"));
}

@Test
Expand Down Expand Up @@ -436,8 +446,10 @@ void escapesXmlSpecialCharacters() throws Exception {
);

String junit = ReportFormatter.format(report(metric), ReportFormat.JUNIT);
String name = "amp&apos'quote\"lt<gt>:1 [CRAP=1.0, CC=1, Cov=100.0% (instruction)]";

assertEquals("amp&apos'quote\"lt<gt>", propertyValue(parseXml(junit), "methodName"));
assertEquals(name, testcaseByName(parseXml(junit), name).getAttribute("name"));
}

@Test
Expand Down Expand Up @@ -525,17 +537,25 @@ private static String propertyValue(Document document, String name) {
throw new AssertionError("Missing XML property: " + name);
}

private static boolean hasTestcase(Document document, String name) {
private static boolean hasTestcaseNameStartingWith(Document document, String prefix) {
NodeList testcases = document.getElementsByTagName("testcase");
for (int index = 0; index < testcases.getLength(); index++) {
Element testcase = (Element) testcases.item(index);
if (name.equals(testcase.getAttribute("name"))) {
if (testcase.getAttribute("name").startsWith(prefix)) {
return true;
}
}
return false;
}

private static String childText(Element element, String tagName) {
NodeList children = element.getElementsByTagName(tagName);
if (children.getLength() == 0) {
throw new AssertionError("Missing XML child: " + tagName);
}
return children.item(0).getTextContent();
}

private static Element testcaseByName(Document document, String name) {
NodeList testcases = document.getElementsByTagName("testcase");
for (int index = 0; index < testcases.getLength(); index++) {
Expand Down