diff --git a/README.md b/README.md index f7170b4..368004d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/core/src/main/java/media/barney/crap/core/ReportFormatter.java b/core/src/main/java/media/barney/crap/core/ReportFormatter.java index 1ea8470..d01a4ae 100644 --- a/core/src/main/java/media/barney/crap/core/ReportFormatter.java +++ b/core/src/main/java/media/barney/crap/core/ReportFormatter.java @@ -263,6 +263,7 @@ 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), @@ -270,8 +271,9 @@ private static JunitTestCase junitTestCase(CrapReport.MethodReport method, method.startLine(), time, junitProperties(method, omitRedundancy), - junitFailure(method, threshold), - junitSkipped(method, threshold) + diagnosticText, + junitFailure(method, threshold, diagnosticText), + junitSkipped(method, diagnosticText) ); } @@ -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 ); } @@ -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) { @@ -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 ) { diff --git a/core/src/test/java/media/barney/crap/core/MainTest.java b/core/src/test/java/media/barney/crap/core/MainTest.java index 8f7da28..ed1f078 100644 --- a/core/src/test/java/media/barney/crap/core/MainTest.java +++ b/core/src/test/java/media/barney/crap/core/MainTest.java @@ -292,9 +292,9 @@ void noneFormatSuppressesStdoutButKeepsJunitSidecarComplete() throws Exception { assertEquals(2, exit); assertEquals("", utf8(out)); assertTrue(junit.contains("")); - assertFalse(hasTestcase(document, "safe:9")); - assertFalse(hasTestcase(document, "unknown:20")); + assertFalse(hasTestcaseNameStartingWith(document, "safe:9 ")); + assertFalse(hasTestcaseNameStartingWith(document, "unknown:20 ")); } @Test @@ -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")); @@ -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 @@ -436,8 +446,10 @@ void escapesXmlSpecialCharacters() throws Exception { ); String junit = ReportFormatter.format(report(metric), ReportFormat.JUNIT); + String name = "amp&apos'quote\"lt:1 [CRAP=1.0, CC=1, Cov=100.0% (instruction)]"; assertEquals("amp&apos'quote\"lt", propertyValue(parseXml(junit), "methodName")); + assertEquals(name, testcaseByName(parseXml(junit), name).getAttribute("name")); } @Test @@ -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++) {