Skip to content

Commit

Permalink
Add support for transforming Checkstyle XML output to JUnit XML (#283)
Browse files Browse the repository at this point in the history
Bazel will natively create a `test.xml` for all test runners if they do
not create one themselves.

For the checkstyle test-runner it would create a test.xml that captures
the system-out.
```xml
<testsuites>
  <testsuite name="server-common/server-common-checkstyleexec" tests="1" failures="0" errors="1">
    <testcase name="server-common/server-common-checkstyleexec" status="run" duration="5" time="5"><error message="exited with error code 1"></error></testcase>
      <system-out>
Generated test.log (if the file is not UTF-8, then this may be unreadable):
<![CDATA[exec ${PAGER:-/usr/bin/less} "$0" || exit 1
Executing tests from //server-common:server-common-checkstyle
-----------------------------------------------------------------------------
Checkstyle ends with 1 errors.
Starting audit...
[ERROR] server-common/src/main/java/org/apache/kafka/queue/KafkaEventQueue.java:30:1: Disallowed import - io.confluent.kafka.util.OpenTelemetryManager. [ImportControl]
Audit done.]]>
      </system-out>
    </testsuite>
</testsuites>
```

JUnit is a well establish format and well known by many CI programs.
Having the checkstyle rule itself create a more curated JUnit XML file
is in the user's best interest.

In this PR, we modify the checkstyle rules to emit a `test.xml` itself,
stopping Bazel from implicitly doing so.
We have checkstyle emit the XML format _always_ and then use a very
minimal Java tool and a provided default XLST (XML stylesheet) to
transform the Checkstyle XML to one following JUnit's XML specification.

```xml
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite package="Checkstyle" name="src/main/java/io/confluent/utils/HelloWorldUtils.java" tests="2" errors="2">

<testcase name="com.puppycrawl.tools.checkstyle.checks.imports.AvoidStarImportCheck" classname="src/main/java/io/confluent/utils/HelloWorldUtils.java">
    <error type="com.puppycrawl.tools.checkstyle.checks.imports.AvoidStarImportCheck" message="Line 3: Using the '.*' form of import should be avoided - java.io.*."/>
</testcase>

<testcase name="com.puppycrawl.tools.checkstyle.checks.javadoc.MissingJavadocMethodCheck" classname="src/main/java/io/confluent/utils/HelloWorldUtils.java">
    <error type="com.puppycrawl.tools.checkstyle.checks.javadoc.MissingJavadocMethodCheck" message="Line 7: Missing a Javadoc comment."/>
</testcase>

</testsuite>
</testsuites>
```
  • Loading branch information
fzakaria authored Aug 16, 2024
1 parent ca23db3 commit e0d10b4
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 10 deletions.
1 change: 1 addition & 0 deletions java/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ alias(
exports_files([
"checkstyle-strict.xml",
"google-checks.xml",
"checkstyle2junit.xslt",
])

checkstyle_config(
Expand Down
46 changes: 46 additions & 0 deletions java/checkstyle2junit.xslt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output encoding="UTF-8" method="xml"></xsl:output>

<xsl:template match="/">
<testsuites>
<testsuite package="Checkstyle">
<xsl:attribute name="name">
<xsl:value-of select="//checkstyle/file/@name" />
</xsl:attribute>
<xsl:attribute name="tests">
<xsl:value-of select="count(.//error)" />
</xsl:attribute>
<xsl:attribute name="errors">
<xsl:value-of select="count(.//error)" />
</xsl:attribute>
<xsl:for-each select="//checkstyle">
<xsl:apply-templates />
</xsl:for-each>
</testsuite>
</testsuites>
</xsl:template>

<xsl:template match="error">
<testcase>
<xsl:attribute name="name">
<xsl:value-of select="@source" />
</xsl:attribute>
<xsl:attribute name="classname">
<xsl:value-of select="../@name" />
</xsl:attribute>
<error>
<xsl:attribute name="type">
<xsl:value-of select="@source" />
</xsl:attribute>
<xsl:attribute name="message">
<xsl:text>Line </xsl:text>
<xsl:value-of select="@line" />
<xsl:text>: </xsl:text>
<xsl:value-of select="@message" />
</xsl:attribute>
</error>
</testcase>
</xsl:template>

</xsl:stylesheet>
32 changes: 22 additions & 10 deletions java/private/checkstyle.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,30 @@ Checkstyle rule implementation
def _checkstyle_impl(ctx):
info = ctx.attr.config[CheckStyleInfo]
config = info.config_file
output_format = info.output_format

config_dir = paths.dirname(config.short_path)
maybe_cd_config_dir = ["cd {}".format(config_dir)] if config_dir else []

script = "\n".join([
"#!/usr/bin/env bash",
"set -o pipefail",
"set -e",
"set +e",
"OLDPWD=$PWD",
] + maybe_cd_config_dir + [
"$OLDPWD/{lib} -f {output_format} -c {config} {srcs} |sed s:$OLDPWD/::g".format(
"$OLDPWD/{lib} -o checkstyle.xml -f xml -c {config} {srcs}".format(
lib = info.checkstyle.short_path,
output_format = output_format,
config = config.basename,
srcs = " ".join(["$OLDPWD/" + f.short_path for f in ctx.files.srcs]),
),
"checkstyle_exit_code=$?",
# Apply sed to the file in place
"sed s:$OLDPWD/::g checkstyle.xml > checkstyle-stripped.xml",
# Run the Java XSLT transformation tool
"$OLDPWD/{xslt_transformer} checkstyle-stripped.xml $OLDPWD/{xslt} > $XML_OUTPUT_FILE".format(
xslt_transformer = ctx.executable._xslt_transformer.short_path,
xslt = ctx.file.xslt.short_path,
),
"exit $checkstyle_exit_code",
])
out = ctx.actions.declare_file(ctx.label.name + "exec")

Expand All @@ -35,8 +42,8 @@ def _checkstyle_impl(ctx):
)

runfiles = ctx.runfiles(
files = ctx.files.srcs + [info.checkstyle],
)
files = ctx.files.srcs + [info.checkstyle, ctx.file.xslt],
).merge(ctx.attr._xslt_transformer[DefaultInfo].default_runfiles)

return [
DefaultInfo(
Expand Down Expand Up @@ -64,10 +71,15 @@ _checkstyle_test = rule(
[CheckStyleInfo],
],
),
"output_format": attr.string(
doc = "Output Format can be plain or xml. Defaults to plain",
values = ["plain", "xml"],
default = "plain",
"xslt": attr.label(
doc = "Path to the checkstyle2junit.xslt file",
allow_single_file = True,
default = "@contrib_rules_jvm//java:checkstyle2junit.xslt",
),
"_xslt_transformer": attr.label(
default = "@contrib_rules_jvm//java/src/com/github/bazel_contrib/contrib_rules_jvm/xml:XSLTTransformer",
executable = True,
cfg = "exec",
),
},
executable = True,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
load("@rules_java//java:defs.bzl", "java_binary", "java_library")

java_library(
name = "xml",
srcs = ["XSLTTransformer.java"],
visibility = ["//:__subpackages__"],
)

java_binary(
name = "XSLTTransformer",
main_class = "XSLTTransformer",
visibility = ["//visibility:public"],
runtime_deps = [":xml"],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import java.io.File;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

public class XSLTTransformer {
public static void main(String[] args) throws Exception {
if (args.length != 2) {
System.err.println("Usage: java XSLTTransformer <input.xml> <transform.xslt>");
System.exit(1);
}

String inputXML = args[0];
String xsltFile = args[1];

// Create transformer factory
TransformerFactory factory = TransformerFactory.newInstance();

// Load the XSLT file
StreamSource xslt = new StreamSource(new File(xsltFile));

// Create a transformer
Transformer transformer = factory.newTransformer(xslt);

// Load the input XML file
StreamSource xmlInput = new StreamSource(new File(inputXML));

// Set the output file
StreamResult xmlOutput = new StreamResult(System.out);

// Perform the transformation
transformer.transform(xmlInput, xmlOutput);
}
}

0 comments on commit e0d10b4

Please sign in to comment.