Skip to content

Commit

Permalink
Add support for software metrics.
Browse files Browse the repository at this point in the history
The latest release of the coverage-model supports reporting of
software metrics. While it does not yet make sense to use them
in autograding, it would be helpful to show the results of the
recorded metrics in the quality monitor.
  • Loading branch information
uhafner committed Oct 27, 2024
1 parent 2fdb33a commit 8fdbbf7
Show file tree
Hide file tree
Showing 19 changed files with 2,256 additions and 142 deletions.
6 changes: 2 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>edu.hm.hafner</groupId>
<artifactId>codingstyle-pom</artifactId>
<version>4.16.0</version>
<version>5.2.0</version>
<relativePath />
</parent>

Expand Down Expand Up @@ -58,9 +58,7 @@

<jackson-databind.version>2.18.0</jackson-databind.version>
<analysis-model.version>12.9.0</analysis-model.version>
<coverage-model.version>0.47.0</coverage-model.version>
<java.version>17</java.version>

<coverage-model.version>0.48.0</coverage-model.version>
</properties>

<dependencies>
Expand Down
114 changes: 88 additions & 26 deletions src/main/java/edu/hm/hafner/grading/AggregatedScore.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import edu.hm.hafner.coverage.Node;
import edu.hm.hafner.grading.AnalysisScore.AnalysisScoreBuilder;
import edu.hm.hafner.grading.CoverageScore.CoverageScoreBuilder;
import edu.hm.hafner.grading.MetricScore.MetricScoreBuilder;
import edu.hm.hafner.grading.TestScore.TestScoreBuilder;
import edu.hm.hafner.util.FilteredLog;
import edu.hm.hafner.util.Generated;
Expand All @@ -43,10 +44,12 @@ public final class AggregatedScore implements Serializable {
private final List<TestScore> testScores = new ArrayList<>();
private final List<CoverageScore> coverageScores = new ArrayList<>();
private final List<AnalysisScore> analysisScores = new ArrayList<>();
private final List<MetricScore> metricScores = new ArrayList<>();

private final List<TestConfiguration> testConfigurations;
private final List<CoverageConfiguration> coverageConfigurations;
private final List<AnalysisConfiguration> analysisConfigurations;
private final List<MetricConfiguration> metricConfigurations;

private static FilteredLog createNullLogger() {
return new FilteredLog("Autograding");
Expand All @@ -68,6 +71,7 @@ public AggregatedScore(final String configuration, final FilteredLog log) {
analysisConfigurations = AnalysisConfiguration.from(configuration);
coverageConfigurations = CoverageConfiguration.from(configuration);
testConfigurations = TestConfiguration.from(configuration);
metricConfigurations = MetricConfiguration.from(configuration);

this.log = log;
}
Expand All @@ -86,7 +90,8 @@ public List<String> getErrorMessages() {
* @return the number of achieved points
*/
public int getAchievedScore() {
return getTestAchievedScore() + getCoverageAchievedScore() + getAnalysisAchievedScore();
return getTestAchievedScore() + getCoverageAchievedScore()
+ getAnalysisAchievedScore() + getMetricAchievedScore();
}

private int getAchievedScore(final List<? extends Score<?, ?>> scores) {
Expand Down Expand Up @@ -128,6 +133,10 @@ public int getAnalysisAchievedScore() {
return getAchievedScore(analysisScores);
}

public int getMetricAchievedScore() {
return getAchievedScore(metricScores);
}

/**
* Returns the total number of points, i.e., the maximum score.
*
Expand Down Expand Up @@ -221,6 +230,19 @@ public boolean hasAnalysis() {
return !analysisConfigurations.isEmpty();
}

public int getMetricsMaxScore() {
return getMaxScore(metricConfigurations);

Check warning on line 234 in src/main/java/edu/hm/hafner/grading/AggregatedScore.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/edu/hm/hafner/grading/AggregatedScore.java#L234

Added line #L234 was not covered by tests
}

/**
* Returns whether at least one metric configuration has been defined.
*
* @return {@code true} if there are metric configurations, {@code false} otherwise
*/
public boolean hasMetrics() {
return !metricConfigurations.isEmpty();
}

/**
* Returns the success ratio, i.e., the number of achieved points divided by total points.
*
Expand Down Expand Up @@ -257,6 +279,10 @@ public int getAnalysisRatio() {
return getRatio(getAnalysisAchievedScore(), getAnalysisMaxScore());
}

public int getMetricRatio() {
return getRatio(getMetricAchievedScore(), getMetricsMaxScore());

Check warning on line 283 in src/main/java/edu/hm/hafner/grading/AggregatedScore.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/edu/hm/hafner/grading/AggregatedScore.java#L283

Added line #L283 was not covered by tests
}

/**
* Returns whether at least one unit test failure has been recorded. In such a case, mutation results will not be
* available.
Expand Down Expand Up @@ -345,6 +371,10 @@ public List<AnalysisScore> getAnalysisScores() {
return List.copyOf(analysisScores);
}

public List<MetricScore> getMetricScores() {
return List.copyOf(metricScores);
}

@Override
@Generated
public boolean equals(final Object o) {
Expand All @@ -359,16 +389,18 @@ public boolean equals(final Object o) {
&& Objects.equals(testScores, that.testScores)
&& Objects.equals(coverageScores, that.coverageScores)
&& Objects.equals(analysisScores, that.analysisScores)
&& Objects.equals(metricScores, that.metricScores)
&& Objects.equals(testConfigurations, that.testConfigurations)
&& Objects.equals(coverageConfigurations, that.coverageConfigurations)
&& Objects.equals(analysisConfigurations, that.analysisConfigurations);
&& Objects.equals(analysisConfigurations, that.analysisConfigurations)
&& Objects.equals(metricConfigurations, that.metricConfigurations);
}

@Override
@Generated
public int hashCode() {
return Objects.hash(log, testScores, coverageScores, analysisScores, testConfigurations, coverageConfigurations,
analysisConfigurations);
return Objects.hash(log, testScores, coverageScores, analysisScores, metricScores, testConfigurations,
coverageConfigurations, analysisConfigurations, metricConfigurations);
}

@Override
Expand Down Expand Up @@ -452,7 +484,7 @@ public void gradeCoverage(final CoverageReportFactory factory) {
* @param factory
* the factory to create the reports
*/
public void gradeTests(final TestReportFactory factory) {
public void gradeTests(final CoverageReportFactory factory) {
log.logInfo("Processing %d test configuration(s)", testConfigurations.size());
for (TestConfiguration testConfiguration : testConfigurations) {
log.logInfo("%s Configuration:%n%s", testConfiguration.getName(), testConfiguration);
Expand Down Expand Up @@ -481,6 +513,41 @@ public void gradeTests(final TestReportFactory factory) {
}
}

/**
* Grades the reports given by the report factory and creates corresponding scores for the metrics.
*
* @param factory
* the factory to create the reports
*/
public void gradeMetrics(final CoverageReportFactory factory) {
log.logInfo("Processing %d metric configuration(s)", metricConfigurations.size());
for (MetricConfiguration metricConfiguration : metricConfigurations) {
log.logInfo("%s Configuration:%n%s", metricConfiguration.getName(), metricConfiguration);

List<MetricScore> scores = new ArrayList<>();
for (ToolConfiguration tool : metricConfiguration.getTools()) {
var report = factory.create(tool, log);
var score = new MetricScoreBuilder()
.withConfiguration(metricConfiguration)
.withId(tool.getId())
.withName(StringUtils.defaultIfBlank(tool.getName(), report.getName()))
.withReport(report, Metric.fromName(tool.getMetric()))
.build();
scores.add(score);
logSubResult(score);
}

var aggregation = new MetricScoreBuilder()
.withConfiguration(metricConfiguration)
.withScores(scores)
.build();

metricScores.add(aggregation);

logResult(metricConfiguration, aggregation);
}
}

private void logSubResult(final Score<?, ?> score) {
if (!score.hasMaxScore()) {
log.logInfo("=> %s: %s",
Expand Down Expand Up @@ -520,6 +587,9 @@ public Map<String, Integer> getMetrics() {
metrics.putAll(getAnalysisTopLevelMetrics());
metrics.putAll(getAnalysisMetrics());
}
if (hasMetrics()) {
metrics.putAll(getSoftwareMetrics());
}
return metrics;
}

Expand All @@ -529,6 +599,13 @@ private Map<String, Integer> getTestMetrics() {
return Map.of("tests", tests);
}

private Map<String, Integer> getSoftwareMetrics() {
return getMetricScores().stream()
.map(Score::getSubScores)
.flatMap(Collection::stream)
.collect(Collectors.toMap(MetricScore::getMetricTagName, MetricScore::getMetricValue));
}

private Map<String, Integer> getCoverageMetrics() {
return getCodeCoverageScores().stream()
.map(Score::getSubScores)
Expand Down Expand Up @@ -556,7 +633,9 @@ private Map<String, Integer> getAnalysisTopLevelMetrics() {
}

/**
* Factory to create the static analysis reports.
* Factory to create the static analysis reports based on the analysis-model.
*
* @see <a href="https://github.com/jenkinsci/analysis-model">Analysis Model</a>
*/
public interface AnalysisReportFactory {
/**
Expand All @@ -575,7 +654,9 @@ public interface AnalysisReportFactory {
}

/**
* Factory to create the coverage reports.
* Factory to create the coverage, test and metric reports that are based on the coverage-model.
*
* @see <a href="https://github.com/jenkinsci/coverage-model">Coverage Model</a>
*/
public interface CoverageReportFactory {
/**
Expand All @@ -592,23 +673,4 @@ public interface CoverageReportFactory {
*/
Node create(ToolConfiguration tool, FilteredLog log);
}

/**
* Factory to create the test reports.
*/
public interface TestReportFactory {
/**
* Creates a test report for the specified tool.
*
* @param tool
* the tool to create the report for
* @param log
* the logger to report the progress
*
* @return the created report
* @throws NoSuchElementException
* if there is no test report for the specified tool
*/
Node create(ToolConfiguration tool, FilteredLog log);
}
}
7 changes: 6 additions & 1 deletion src/main/java/edu/hm/hafner/grading/AutoGradingRunner.java
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ public AggregatedScore run() {
score.gradeAnalysis(new FileSystemAnalysisReportFactory());
logHandler.print();

log.logInfo(DOUBLE_LINE);

Check warning on line 98 in src/main/java/edu/hm/hafner/grading/AutoGradingRunner.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/edu/hm/hafner/grading/AutoGradingRunner.java#L98

Added line #L98 was not covered by tests

score.gradeMetrics(new FileSystemCoverageReportFactory());
logHandler.print();

Check warning on line 101 in src/main/java/edu/hm/hafner/grading/AutoGradingRunner.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/edu/hm/hafner/grading/AutoGradingRunner.java#L100-L101

Added lines #L100 - L101 were not covered by tests

log.logInfo(DOUBLE_LINE);
var results = new GradingReport();
log.logInfo(results.getTextSummary(score));
Expand Down Expand Up @@ -215,7 +220,7 @@ protected String createErrorMessageMarkdown(final FilteredLog log) {
if (log.hasErrors()) {
var errors = new StringBuilder(ERROR_CAPACITY);

errors.append("\n### :construction: Error Messages\n\n```\n");
errors.append("\n### :construction: &nbsp; Error Messages\n\n```\n");

Check warning on line 223 in src/main/java/edu/hm/hafner/grading/AutoGradingRunner.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/edu/hm/hafner/grading/AutoGradingRunner.java#L223

Added line #L223 was not covered by tests
var messages = new StringJoiner("\n");
log.getErrorMessages().forEach(messages::add);
errors.append(messages);
Expand Down
25 changes: 18 additions & 7 deletions src/main/java/edu/hm/hafner/grading/CoverageScore.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public final class CoverageScore extends Score<CoverageScore, CoverageConfigurat
@Serial
private static final long serialVersionUID = 3L;

private static final Metric AGGREGATION_METRIC = Metric.CONTAINER;

private final int coveredPercentage;
private final Metric metric;
private final String icon;
Expand All @@ -44,19 +46,19 @@ private CoverageScore(final String id, final String name, final CoverageConfigur
this.coveredPercentage = scores.stream()
.reduce(0, (sum, score) -> sum + score.getCoveredPercentage(), Integer::sum)
/ scores.size();
this.missedItems = scores.stream()
.reduce(0, (sum, score) -> sum + score.getMissedItems(), Integer::sum);
var metrics = scores.stream()
.map(CoverageScore::getMetric)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
this.missedItems = scores.stream()
.reduce(0, (sum, score) -> sum + score.getMissedItems(), Integer::sum);
if (metrics.size() > 1) {
this.metric = Metric.FILE;
this.metric = AGGREGATION_METRIC; // cannot aggregate different metrics
}
else {
this.metric = metrics.iterator().next();
}
this.icon = selectIcon(Metric.FILE);
this.icon = selectIcon(this.metric);

this.report = new ContainerNode(name);
scores.stream().map(CoverageScore::getReport).forEach(report::addChild);
Expand Down Expand Up @@ -97,12 +99,15 @@ private String selectIcon(final Metric iconMetric) {
case LINE -> {
return "wavy_dash";
}
case COMPLEXITY -> {
case CYCLOMATIC_COMPLEXITY -> {
return "part_alternation_mark";
}
case LOC -> {
return "pencil2";
}
case TEST_STRENGTH -> {
return "muscle";

Check warning on line 109 in src/main/java/edu/hm/hafner/grading/CoverageScore.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/edu/hm/hafner/grading/CoverageScore.java#L109

Added line #L109 was not covered by tests
}
default -> {
return "footprints";
}
Expand Down Expand Up @@ -165,19 +170,22 @@ private String getItemName() {
case MUTATION -> {
return "survived mutations";
}
case TEST_STRENGTH -> {
return "survived mutations in tested code";

Check warning on line 174 in src/main/java/edu/hm/hafner/grading/CoverageScore.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/edu/hm/hafner/grading/CoverageScore.java#L174

Added line #L174 was not covered by tests
}
case BRANCH -> {
return "missed branches";
}
case LINE -> {
return "missed lines";
}
case COMPLEXITY -> {
case CYCLOMATIC_COMPLEXITY -> {
return "complexity";
}
case LOC -> {
return "lines of code";
}
case FILE -> {
case CONTAINER -> {
return "missed items";
}
default -> {
Expand Down Expand Up @@ -294,6 +302,9 @@ private CoverageConfiguration getConfiguration() {
public CoverageScoreBuilder withReport(final Node rootNode, final Metric metric) {
this.report = rootNode;
this.metric = metric;

Ensure.that(metric.isCoverage()).isTrue("The metric must be a coverage metric, but is %s", metric);

return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ private ContainerNode createEmptyContainer(final ToolConfiguration tool) {
return new ContainerNode(tool.getName());
}

private String extractMetric(final ToolConfiguration tool, final Node node) {
return node.getValue(Metric.valueOf(StringUtils.upperCase(tool.getMetric())))
String extractMetric(final ToolConfiguration tool, final Node node) {
return node.getValue(Metric.fromName(tool.getMetric()))
.map(Value::toString)
.orElse("<none>");
}
Expand Down
Loading

0 comments on commit 8fdbbf7

Please sign in to comment.