Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added PMD Metrics Parser #119

Merged
merged 6 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,6 @@ This library consists basically of two separate parts:
* [JUnit](https://junit.org/junit5/) test results
* [NUnit](https://nunit.org) test results
* [XUnit](https://xunit.net) test results
* [PMD](https://pmd.github.io/) Metrics XML report
maxwai marked this conversation as resolved.
Show resolved Hide resolved

All source code is licensed under the MIT license. Contributions to this library are welcome!
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public CyclomaticComplexity(final int complexity, final Metric metric) {
}

@Override
protected IntegerValue create(final int value) {
return new CyclomaticComplexity(value);
protected IntegerValue create(final int value, final Metric metric) {
return new CyclomaticComplexity(value, metric);
uhafner marked this conversation as resolved.
Show resolved Hide resolved
}
}
4 changes: 2 additions & 2 deletions src/main/java/edu/hm/hafner/coverage/IntegerValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ public int getValue() {

@Override
public IntegerValue add(final Value other) {
return castAndMap(other, o -> create(integer + o.getValue()));
return castAndMap(other, o -> create(integer + o.getValue(), o.getMetric()));
}

protected abstract IntegerValue create(int value);
protected abstract IntegerValue create(int value, Metric metric);

@Override
public IntegerValue max(final Value other) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/edu/hm/hafner/coverage/LinesOfCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public LinesOfCode(final int loc) {
}

@Override
protected IntegerValue create(final int value) {
protected IntegerValue create(final int value, final Metric ignored) {
return new LinesOfCode(value);
}
}
5 changes: 4 additions & 1 deletion src/main/java/edu/hm/hafner/coverage/Metric.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ public enum Metric {
COMPLEXITY_MAXIMUM(new MethodMaxComplexityFinder(), MetricTendency.SMALLER_IS_BETTER),
COMPLEXITY_DENSITY(new DensityEvaluator(), MetricTendency.SMALLER_IS_BETTER),
LOC(new LocEvaluator(), MetricTendency.SMALLER_IS_BETTER),
TESTS(new ValuesAggregator(), MetricTendency.LARGER_IS_BETTER);
TESTS(new ValuesAggregator(), MetricTendency.LARGER_IS_BETTER),
NCSS(new ValuesAggregator(), MetricTendency.SMALLER_IS_BETTER),
COGNITIVE_COMPLEXITY(new ValuesAggregator(), MetricTendency.SMALLER_IS_BETTER),
NPATH_COMPLEXITY(new ValuesAggregator(), MetricTendency.SMALLER_IS_BETTER);

/**
* Returns the metric that belongs to the specified tag.
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/edu/hm/hafner/coverage/TestCount.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public TestCount(final int tests) {
}

@Override
protected IntegerValue create(final int value) {
protected IntegerValue create(final int value, final Metric ignored) {
return new TestCount(value);
}
}
6 changes: 6 additions & 0 deletions src/main/java/edu/hm/hafner/coverage/Value.java
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ public static Value valueOf(final String stringRepresentation) {
return new LinesOfCode(Integer.parseInt(value));
case TESTS:
return new TestCount(Integer.parseInt(value));
case NCSS:
return new CyclomaticComplexity(Integer.parseInt(value), Metric.NCSS);
case NPATH_COMPLEXITY:
return new CyclomaticComplexity(Integer.parseInt(value), Metric.NPATH_COMPLEXITY);
case COGNITIVE_COMPLEXITY:
return new CyclomaticComplexity(Integer.parseInt(value), Metric.COGNITIVE_COMPLEXITY);
}
}
}
Expand Down
243 changes: 243 additions & 0 deletions src/main/java/edu/hm/hafner/coverage/parser/MetricsParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
package edu.hm.hafner.coverage.parser;

import java.io.Reader;
import java.nio.file.Paths;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;

import com.google.errorprone.annotations.CanIgnoreReturnValue;

import edu.hm.hafner.coverage.ClassNode;
import edu.hm.hafner.coverage.CoverageParser;
import edu.hm.hafner.coverage.CyclomaticComplexity;
import edu.hm.hafner.coverage.FileNode;
import edu.hm.hafner.coverage.MethodNode;
import edu.hm.hafner.coverage.Metric;
import edu.hm.hafner.coverage.ModuleNode;
import edu.hm.hafner.coverage.Node;
import edu.hm.hafner.coverage.PackageNode;
import edu.hm.hafner.util.FilteredLog;
import edu.hm.hafner.util.PathUtil;
import edu.hm.hafner.util.SecureXmlParserFactory;
import edu.hm.hafner.util.SecureXmlParserFactory.ParsingException;
import edu.hm.hafner.util.TreeString;

/**
* Parses Metrics reports into a hierarchical Java Object Model.
*
* @author Maximilian Waidelich
*/
@SuppressWarnings("PMD.GodClass")
public class MetricsParser extends CoverageParser {
private static final long serialVersionUID = -4461747681863455621L;

/** XML elements. */
private static final QName PACKAGE = new QName("package");
private static final QName CLASS = new QName("class");
private static final QName METHOD = new QName("method");
private static final QName METRIC = new QName("metric");
private static final QName FILE = new QName("file");

/** Required attributes of the XML elements. */
private static final QName FQCN = new QName("fqcn");
private static final QName NAME = new QName("name");
private static final QName BEGIN_LINE = new QName("beginline");
private static final QName VALUE = new QName("value");

private static final String CYCLOMATIC_COMPLEXITY = "CyclomaticComplexity";
private static final String COGNITIVE_COMPLEXITY = "CognitiveComplexity";
private static final String NCSS = "NCSS";
private static final String NPATH_COMPLEXITY = "NPathComplexity";

private static final PathUtil PATH_UTIL = new PathUtil();

/**
* Creates a new instance of {@link MetricsParser}.
*/
public MetricsParser() {
this(ProcessingMode.FAIL_FAST);
}

Check warning on line 62 in src/main/java/edu/hm/hafner/coverage/parser/MetricsParser.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 61-62 are not covered by tests

/**
* Creates a new instance of {@link MetricsParser}.
*
* @param processingMode
* determines whether to ignore errors
*/
public MetricsParser(final ProcessingMode processingMode) {
super(processingMode);
}

@Override
protected ModuleNode parseReport(final Reader reader, final String fileName, final FilteredLog log) {
try {
var factory = new SecureXmlParserFactory();
var eventReader = factory.createXmlEventReader(reader);

while (eventReader.hasNext()) {
XMLEvent event = eventReader.nextEvent();

if (event.isStartElement()) {
var startElement = event.asStartElement();
var tagName = startElement.getName();
if (PACKAGE.equals(tagName)) {
var root = new ModuleNode("");
maxwai marked this conversation as resolved.
Show resolved Hide resolved
readPackage(eventReader, root, startElement, fileName);
return root;
}
}
}
handleEmptyResults(fileName, log);

return new ModuleNode("empty");
}
catch (XMLStreamException exception) {
throw new ParsingException(exception);
}
}

@CanIgnoreReturnValue
private PackageNode readPackage(final XMLEventReader reader, final ModuleNode root,
final StartElement startElement, final String fileName) throws XMLStreamException {
var packageName = getValueOf(startElement, FQCN);
var packageNode = root.findOrCreatePackageNode(packageName.replaceAll("\\.", "/"));
maxwai marked this conversation as resolved.
Show resolved Hide resolved
while (reader.hasNext()) {

Check warning on line 107 in src/main/java/edu/hm/hafner/coverage/parser/MetricsParser.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 107 is only partially covered, one branch is missing
XMLEvent event = reader.nextEvent();

if (event.isStartElement()) {
var nextElement = event.asStartElement();
if (FILE.equals(nextElement.getName())) {
readSourceFile(reader, packageNode, nextElement, fileName);
}
else if (METRIC.equals(nextElement.getName())) {

Check warning on line 115 in src/main/java/edu/hm/hafner/coverage/parser/MetricsParser.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 115 is only partially covered, one branch is missing
readValueCounter(packageNode, nextElement);
}
}
else if (event.isEndElement()) {
var endElement = event.asEndElement();
if (PACKAGE.equals(endElement.getName())) {

Check warning on line 121 in src/main/java/edu/hm/hafner/coverage/parser/MetricsParser.java

View check run for this annotation

ci.jenkins.io / Mutation Coverage

Mutation survived

One mutation survived in line 121 (NegateConditionalsMutator)
Raw output
Survived mutations:
- negated conditional (org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator)
return packageNode;

Check warning on line 122 in src/main/java/edu/hm/hafner/coverage/parser/MetricsParser.java

View check run for this annotation

ci.jenkins.io / Mutation Coverage

Mutation survived

One mutation survived in line 122 (NullReturnValsMutator)
Raw output
Survived mutations:
- replaced return value with null for edu/hm/hafner/coverage/parser/MetricsParser::readPackage (org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator)
}
}
}
throw createEofException(fileName);

Check warning on line 126 in src/main/java/edu/hm/hafner/coverage/parser/MetricsParser.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 126 is not covered by tests
}

@CanIgnoreReturnValue
private Node readClass(final XMLEventReader reader, final FileNode fileNode, final StartElement startElement,
final String fileName, final PackageNode packageNode) throws XMLStreamException {
ClassNode classNode = fileNode.findOrCreateClassNode(packageNode.getName() + "." + getValueOf(startElement, NAME));
while (reader.hasNext()) {

Check warning on line 133 in src/main/java/edu/hm/hafner/coverage/parser/MetricsParser.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 133 is only partially covered, one branch is missing
XMLEvent event = reader.nextEvent();

if (event.isStartElement()) {
var nextElement = event.asStartElement();
if (METHOD.equals(nextElement.getName())) {
readMethod(reader, classNode, nextElement, fileName);
}
else if (METRIC.equals(nextElement.getName())) {

Check warning on line 141 in src/main/java/edu/hm/hafner/coverage/parser/MetricsParser.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 141 is only partially covered, one branch is missing
readValueCounter(classNode, nextElement);
}
}
else if (event.isEndElement()) {
var endElement = event.asEndElement();
if (CLASS.equals(endElement.getName())) {
return classNode;

Check warning on line 148 in src/main/java/edu/hm/hafner/coverage/parser/MetricsParser.java

View check run for this annotation

ci.jenkins.io / Mutation Coverage

Mutation survived

One mutation survived in line 148 (NullReturnValsMutator)
Raw output
Survived mutations:
- replaced return value with null for edu/hm/hafner/coverage/parser/MetricsParser::readClass (org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator)
}
}
}
throw createEofException(fileName);

Check warning on line 152 in src/main/java/edu/hm/hafner/coverage/parser/MetricsParser.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 152 is not covered by tests
}

private TreeString internPath(final String filePath) {
return getTreeStringBuilder().intern(PATH_UTIL.getRelativePath(Paths.get(filePath)));
}

@CanIgnoreReturnValue
private Node readSourceFile(final XMLEventReader reader, final PackageNode packageNode,
final StartElement startElement, final String fileName)
throws XMLStreamException {
var sourceFilePath = Paths.get(getValueOf(startElement, NAME)).getFileName();
String sourceFilefileName;
maxwai marked this conversation as resolved.
Show resolved Hide resolved
if (sourceFilePath == null) {

Check warning on line 165 in src/main/java/edu/hm/hafner/coverage/parser/MetricsParser.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 165 is only partially covered, one branch is missing
sourceFilefileName = getValueOf(startElement, NAME);

Check warning on line 166 in src/main/java/edu/hm/hafner/coverage/parser/MetricsParser.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 166 is not covered by tests
}
else {
sourceFilefileName = sourceFilePath.toString();
}
maxwai marked this conversation as resolved.
Show resolved Hide resolved
var fileNode = packageNode.findOrCreateFileNode(sourceFilefileName,
internPath(getValueOf(startElement, NAME)));

while (reader.hasNext()) {

Check warning on line 174 in src/main/java/edu/hm/hafner/coverage/parser/MetricsParser.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 174 is only partially covered, one branch is missing
XMLEvent event = reader.nextEvent();

if (event.isStartElement()) {
var nextElement = event.asStartElement();
if (CLASS.equals(nextElement.getName())) {
readClass(reader, fileNode, nextElement, fileName, packageNode);
}
else if (METRIC.equals(nextElement.getName())) {

Check warning on line 182 in src/main/java/edu/hm/hafner/coverage/parser/MetricsParser.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 182 is only partially covered, one branch is missing
readValueCounter(fileNode, nextElement);
}
}
else if (event.isEndElement()) {
var endElement = event.asEndElement();
if (FILE.equals(endElement.getName())) {
return fileNode;

Check warning on line 189 in src/main/java/edu/hm/hafner/coverage/parser/MetricsParser.java

View check run for this annotation

ci.jenkins.io / Mutation Coverage

Mutation survived

One mutation survived in line 189 (NullReturnValsMutator)
Raw output
Survived mutations:
- replaced return value with null for edu/hm/hafner/coverage/parser/MetricsParser::readSourceFile (org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator)
}
}
}
throw createEofException(fileName);

Check warning on line 193 in src/main/java/edu/hm/hafner/coverage/parser/MetricsParser.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 193 is not covered by tests
}

@CanIgnoreReturnValue
private Node readMethod(final XMLEventReader reader, final ClassNode classNode,
final StartElement startElement, final String fileName) throws XMLStreamException {
String methodName = getValueOf(startElement, NAME) + "#" + getValueOf(startElement, BEGIN_LINE);

MethodNode methodNode = createMethod(startElement, methodName);
classNode.addChild(methodNode);

while (reader.hasNext()) {

Check warning on line 204 in src/main/java/edu/hm/hafner/coverage/parser/MetricsParser.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 204 is only partially covered, one branch is missing
XMLEvent event = reader.nextEvent();

if (event.isStartElement()) {
var nextElement = event.asStartElement();
if (METRIC.equals(nextElement.getName())) {

Check warning on line 209 in src/main/java/edu/hm/hafner/coverage/parser/MetricsParser.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 209 is only partially covered, one branch is missing
readValueCounter(methodNode, nextElement);
}
}
else if (event.isEndElement()) {
var endElement = event.asEndElement();
if (METHOD.equals(endElement.getName())) {
return methodNode;

Check warning on line 216 in src/main/java/edu/hm/hafner/coverage/parser/MetricsParser.java

View check run for this annotation

ci.jenkins.io / Mutation Coverage

Mutation survived

One mutation survived in line 216 (NullReturnValsMutator)
Raw output
Survived mutations:
- replaced return value with null for edu/hm/hafner/coverage/parser/MetricsParser::readMethod (org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator)
}
}
}
throw createEofException(fileName);

Check warning on line 220 in src/main/java/edu/hm/hafner/coverage/parser/MetricsParser.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 220 is not covered by tests
}

private MethodNode createMethod(final StartElement startElement, final String methodName) {
return new MethodNode(methodName, "", parseInteger(getValueOf(startElement, BEGIN_LINE)));
}

private void readValueCounter(final Node node, final StartElement startElement) {
String currentType = getValueOf(startElement, NAME);
int value = parseInteger(getValueOf(startElement, VALUE));
if (CYCLOMATIC_COMPLEXITY.equals(currentType)) {
uhafner marked this conversation as resolved.
Show resolved Hide resolved
node.addValue(new CyclomaticComplexity(value));
}
else if (COGNITIVE_COMPLEXITY.equals(currentType)) {
node.addValue(new CyclomaticComplexity(value, Metric.COGNITIVE_COMPLEXITY));
}
else if (NCSS.equals(currentType)) {
node.addValue(new CyclomaticComplexity(value, Metric.NCSS));
}
else if (NPATH_COMPLEXITY.equals(currentType)) {

Check warning on line 239 in src/main/java/edu/hm/hafner/coverage/parser/MetricsParser.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 239 is only partially covered, one branch is missing
node.addValue(new CyclomaticComplexity(value, Metric.NPATH_COMPLEXITY));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import edu.hm.hafner.coverage.parser.CoberturaParser;
import edu.hm.hafner.coverage.parser.JacocoParser;
import edu.hm.hafner.coverage.parser.JunitParser;
import edu.hm.hafner.coverage.parser.MetricsParser;
import edu.hm.hafner.coverage.parser.NunitParser;
import edu.hm.hafner.coverage.parser.OpenCoverParser;
import edu.hm.hafner.coverage.parser.PitestParser;
Expand All @@ -28,7 +29,8 @@ public enum CoverageParserType {
PIT,
JUNIT,
VECTORCAST,
XUNIT
XUNIT,
METRICS
}

/**
Expand Down Expand Up @@ -79,6 +81,8 @@ public CoverageParser get(final CoverageParserType parser, final ProcessingMode
return new XunitParser(processingMode);
case VECTORCAST:
return new VectorCastParser(processingMode);
case METRICS:
return new MetricsParser(processingMode);
}
throw new IllegalArgumentException("Unknown parser type: " + parser);
}
Expand Down
6 changes: 3 additions & 3 deletions src/test/java/edu/hm/hafner/coverage/IntegerValueTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ void shouldCreateValue() {
var value = createValue(123);
assertThat(value.serialize()).startsWith(value.getMetric().name()).endsWith(": 123");

assertThat(value.create(100)).hasValue(100);
assertThat(value.create(-100)).hasValue(-100);
assertThat(value.create(0)).hasValue(0);
assertThat(value.create(100, Metric.COMPLEXITY)).hasValue(100);
assertThat(value.create(-100, Metric.COMPLEXITY)).hasValue(-100);
assertThat(value.create(0, Metric.COMPLEXITY)).hasValue(0);
}

@Test
Expand Down
17 changes: 16 additions & 1 deletion src/test/java/edu/hm/hafner/coverage/ValueTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ void shouldReturnCorrectValueOfIntegerValues() {
.isInstanceOfSatisfying(LinesOfCode.class, value -> assertThat(value).hasValue(2));
assertThat(Value.valueOf("TESTS: 3"))
.isInstanceOfSatisfying(TestCount.class, value -> assertThat(value).hasValue(3));
assertThat(Value.valueOf("NCSS: 4"))
.isInstanceOfSatisfying(CyclomaticComplexity.class, value -> assertThat(value).hasValue(4));
assertThat(Value.valueOf("NPATH_COMPLEXITY: 5"))
.isInstanceOfSatisfying(CyclomaticComplexity.class, value -> assertThat(value).hasValue(5));
assertThat(Value.valueOf("COGNITIVE_COMPLEXITY: 6"))
.isInstanceOfSatisfying(CyclomaticComplexity.class, value -> assertThat(value).hasValue(6));
}

@Test
Expand Down Expand Up @@ -88,13 +94,22 @@ void shouldThrowExceptionOnInvalidStringRepresentation() {
void shouldGetValue() {
var linesOfCode = new LinesOfCode(10);
var cyclomaticComplexity = new CyclomaticComplexity(20);
var ncss = new CyclomaticComplexity(30, Metric.NCSS);
var npathComplexity = new CyclomaticComplexity(40, Metric.NPATH_COMPLEXITY);
var coginitiveComplexity = new CyclomaticComplexity(50, Metric.COGNITIVE_COMPLEXITY);

List<Value> values = List.of(linesOfCode, cyclomaticComplexity);
List<Value> values = List.of(linesOfCode, cyclomaticComplexity, ncss, npathComplexity, coginitiveComplexity);

assertThat(Value.getValue(Metric.LOC, values))
.isEqualTo(linesOfCode);
assertThat(Value.getValue(Metric.COMPLEXITY, values))
.isEqualTo(cyclomaticComplexity);
assertThat(Value.getValue(Metric.NCSS, values))
.isEqualTo(ncss);
assertThat(Value.getValue(Metric.NPATH_COMPLEXITY, values))
.isEqualTo(npathComplexity);
assertThat(Value.getValue(Metric.COGNITIVE_COMPLEXITY, values))
.isEqualTo(coginitiveComplexity);
assertThatExceptionOfType(NoSuchElementException.class)
.isThrownBy(() -> Value.getValue(Metric.LINE, values))
.withMessageContaining("No value for metric");
Expand Down
Loading
Loading