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

Add relative test success impact in test configurations #386

Merged
merged 2 commits into from
Oct 16, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
public final class AnalysisConfiguration extends Configuration {
@Serial
private static final long serialVersionUID = 3L;

private static final String ANALYSIS_ID = "analysis";

/**
Expand Down Expand Up @@ -57,6 +58,12 @@ public boolean isPositive() {
return errorImpact >= 0 && highImpact >= 0 && normalImpact >= 0 && lowImpact >= 0;
}

@Override
@JsonIgnore
public boolean hasImpact() {
return errorImpact != 0 || highImpact != 0 || normalImpact != 0 || lowImpact != 0;
}

public int getErrorImpact() {
return errorImpact;
}
Expand Down
41 changes: 29 additions & 12 deletions src/main/java/edu/hm/hafner/grading/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
*
* @author Ullrich Hafner
*/
// TODO: make sure that the configuration is valid
public abstract class Configuration implements Serializable {
@Serial
private static final long serialVersionUID = 3L;
Expand All @@ -35,11 +34,8 @@
var configurations = jackson.readJson(json);
if (configurations.has(id)) {
var deserialized = deserialize(id, type, configurations, jackson);
if (deserialized.isEmpty()) {
throw new IllegalArgumentException("Configuration ID '" + id + "' is empty in JSON: " + json);
}
deserialized.forEach(Configuration::validateDefaults);
// deserialized.forEach(Configuration::validate);
deserialized.forEach(Configuration::validate);
return deserialized;
}
return Collections.emptyList();
Expand Down Expand Up @@ -119,16 +115,37 @@

private void validateDefaults() {
if (tools.isEmpty()) {
throw new IllegalArgumentException("Configuration ID '" + getId() + "' has no tools");
throwIllegalArgumentException("Configuration ID '" + getId() + "' has no tools");

Check warning on line 118 in src/main/java/edu/hm/hafner/grading/Configuration.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/edu/hm/hafner/grading/Configuration.java#L118

Added line #L118 was not covered by tests
}
if (getMaxScore() == 0 && hasImpact()) {
throwIllegalArgumentException("When configuring impacts then the score must not be zero.");

Check warning on line 121 in src/main/java/edu/hm/hafner/grading/Configuration.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/edu/hm/hafner/grading/Configuration.java#L121

Added line #L121 was not covered by tests
}
if (getMaxScore() > 0 && !hasImpact()) {
throwIllegalArgumentException(

Check warning on line 124 in src/main/java/edu/hm/hafner/grading/Configuration.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/edu/hm/hafner/grading/Configuration.java#L124

Added line #L124 was not covered by tests
"When configuring a max score than an impact must be defined as well.");
}
}

private void throwIllegalArgumentException(final String errorMessage) {
throw new IllegalArgumentException(errorMessage + "\nConfiguration: " + this);
}

// /**
// * Validates this configuration.
// *
// * @throws IllegalArgumentException if this configuration is invalid
// */
// protected abstract void validate();
/**
* Returns whether the specified configuration has impact properties defined, or not.
*
* @return {@code true} if the configuration has impact properties, {@code false} if not
*/
protected abstract boolean hasImpact();

/**
* Validates this configuration. This default implementation does nothing. Overwrite this method in subclasses to
* add specific validation logic.
*
* @throws IllegalArgumentException if this configuration is invalid
*/
protected void validate() {
// empty default implementation
}

@Override
@Generated
Expand Down
19 changes: 12 additions & 7 deletions src/main/java/edu/hm/hafner/grading/CoverageConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,27 @@
public final class CoverageConfiguration extends Configuration {
@Serial
private static final long serialVersionUID = 3L;

private static final String COVERAGE_ID = "coverage";
private static final String[] MUTATION_IDS = {"pitest", "mutation", "pit"};
static final String CODE_COVERAGE = "Code Coverage";

/**
* Converts the specified JSON object to a list of {@link CoverageConfiguration} instances.
*
* @param json
* the json object to convert
* the JSON object to convert
*
* @return the corresponding {@link CoverageConfiguration} instances
*/
public static List<CoverageConfiguration> from(final String json) {
return extractConfigurations(json, COVERAGE_ID, CoverageConfiguration.class);
}

@SuppressWarnings("unused") // Json property
@SuppressWarnings("unused") // JSON property
private int coveredPercentageImpact;

@SuppressWarnings("unused") // Json property
@SuppressWarnings("unused") // JSON property
private int missedPercentageImpact;

private CoverageConfiguration() {
Expand All @@ -52,10 +54,7 @@ protected String getDefaultId() {

@Override
protected String getDefaultName() {
if (StringUtils.containsAnyIgnoreCase(getId(), MUTATION_IDS)) {
return "Mutation Coverage";
}
return "Code Coverage";
return CODE_COVERAGE;
}

@Override
Expand All @@ -64,6 +63,12 @@ public boolean isPositive() {
return coveredPercentageImpact >= 0 && missedPercentageImpact >= 0;
}

@Override
@JsonIgnore
public boolean hasImpact() {
return coveredPercentageImpact != 0 || missedPercentageImpact != 0;
}

public int getCoveredPercentageImpact() {
return coveredPercentageImpact;
}
Expand Down
60 changes: 56 additions & 4 deletions src/main/java/edu/hm/hafner/grading/TestConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import java.util.List;
import java.util.Objects;

import com.fasterxml.jackson.annotation.JsonIgnore;

import edu.hm.hafner.util.Generated;

/**
Expand All @@ -15,13 +17,14 @@
public final class TestConfiguration extends Configuration {
@Serial
private static final long serialVersionUID = 3L;

private static final String TEST_ID = "tests";

/**
* Converts the specified JSON object to a list of {@link TestConfiguration} instances.
*
* @param json
* the json object to convert
* the JSON object to convert
*
* @return the corresponding {@link TestConfiguration} instances
*/
Expand All @@ -33,6 +36,9 @@ public static List<TestConfiguration> from(final String json) {
private int passedImpact;
private int skippedImpact;

private int successRateImpact;
private int failureRateImpact;

private TestConfiguration() {
super(); // Instances are created via JSON deserialization
}
Expand All @@ -47,9 +53,36 @@ protected String getDefaultName() {
return "Tests";
}

/**
* Returns whether this configuration defines relative impacts.
*
* @return {@code true} if this configuration defines relative impacts, {@code false} if it defines absolute impacts
*/
@JsonIgnore
public boolean isRelative() {
return getFailureRateImpact() != 0 || getSuccessRateImpact() != 0;
}

/**
* Returns whether this configuration defines absolute impacts.
*
* @return {@code true} if this configuration defines absolute impacts, {@code false} if it defines relative impacts
*/
@JsonIgnore
public boolean isAbsolute() {
return getPassedImpact() != 0 || getFailureImpact() != 0 || getSkippedImpact() != 0;
}

@Override
@JsonIgnore
public boolean isPositive() {
return passedImpact >= 0 && failureImpact >= 0 && skippedImpact >= 0;
return getPassedImpact() >= 0 && getFailureImpact() >= 0 && getSkippedImpact() >= 0;
}

@Override
@JsonIgnore
protected boolean hasImpact() {
return isRelative() || isAbsolute();
}

public int getSkippedImpact() {
Expand All @@ -64,6 +97,22 @@ public int getPassedImpact() {
return passedImpact;
}

public int getSuccessRateImpact() {
return successRateImpact;
}

public int getFailureRateImpact() {
return failureRateImpact;
}

@Override
protected void validate() {
if (isRelative() && isAbsolute()) {
throw new IllegalArgumentException(
"Test configuration must either define an impact for absolute or relative metrics only.");
}
}

@Override
@Generated
public boolean equals(final Object o) {
Expand All @@ -79,12 +128,15 @@ public boolean equals(final Object o) {
var that = (TestConfiguration) o;
return failureImpact == that.failureImpact
&& passedImpact == that.passedImpact
&& skippedImpact == that.skippedImpact;
&& skippedImpact == that.skippedImpact
&& successRateImpact == that.successRateImpact
&& failureRateImpact == that.failureRateImpact;
}

@Override
@Generated
public int hashCode() {
return Objects.hash(super.hashCode(), failureImpact, passedImpact, skippedImpact);
return Objects.hash(super.hashCode(), failureImpact, passedImpact, skippedImpact, successRateImpact,
failureRateImpact);
}
}
35 changes: 31 additions & 4 deletions src/main/java/edu/hm/hafner/grading/TestScore.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public final class TestScore extends Score<TestScore, TestConfiguration> {
private final int passedSize;
private final int failedSize;
private final int skippedSize;

private transient Node report; // do not persist the tree of nodes

private TestScore(final String id, final String name, final TestConfiguration configuration,
Expand Down Expand Up @@ -100,13 +101,39 @@ public int getImpact() {

int change = 0;

change = change + configuration.getPassedImpact() * getPassedSize();
change = change + configuration.getFailureImpact() * getFailedSize();
change = change + configuration.getSkippedImpact() * getSkippedSize();

if (configuration.isAbsolute()) {
change = change + configuration.getPassedImpact() * getPassedSize();
change = change + configuration.getFailureImpact() * getFailedSize();
change = change + configuration.getSkippedImpact() * getSkippedSize();
}
else {
change = change + configuration.getSuccessRateImpact() * getSuccessRate();
change = change + configuration.getFailureRateImpact() * getFailureRate();
}
return change;
}

/**
* Returns the success rate of the tests.
*
* @return the success rate, i.e., the number of passed tests in percent with respect to the total number of tests
*/
public int getSuccessRate() {
var rate = getRateOf(getPassedSize());
if (rate == 100 && getFailedSize() > 0) {
return 99; // 100% success rate is only possible if there are no failed tests
}
return rate;
}

public int getFailureRate() {
return getRateOf(getFailedSize());
}

private int getRateOf(final int achieved) {
return Math.toIntExact(Math.round(achieved * 100.0 / getTotalSize()));
}

public int getPassedSize() {
return passedSize;
}
Expand Down
38 changes: 38 additions & 0 deletions src/test/java/edu/hm/hafner/grading/AbstractConfigurationTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package edu.hm.hafner.grading;

import java.util.List;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;

import static org.assertj.core.api.Assertions.*;

abstract class AbstractConfigurationTest {
@Test
void shouldReportInvalidConfigurations() {
assertThatIllegalArgumentException()
.isThrownBy(() -> fromJson(getInvalidJson()))
.withMessageContaining("Can't convert JSON")
.withCauseInstanceOf(JsonParseException.class);
}

@Test
void shouldReportEmptyConfigurations() {
assertThatIllegalArgumentException()
.isThrownBy(() -> fromJson("""
{
"tests": false,
"analysis": false,
"coverage": false
}
"""))
.withMessageContaining("Can't convert JSON")
.withCauseInstanceOf(JsonMappingException.class);
}

protected abstract List<? extends Configuration> fromJson(String json);

protected abstract String getInvalidJson();
}
Loading