Skip to content

Commit

Permalink
CoveredTestResultPerTestMethod API (#95)
Browse files Browse the repository at this point in the history
* Partially implement CoveredTestResultPerTestMethod API

Signed-off-by: André Silva <andre15andre@hotmail.com>

* Add assertions for coverage map

Signed-off-by: André Silva <andre15andre@hotmail.com>

* Rename new runner classes

Signed-off-by: André Silva <andre15andre@hotmail.com>

* Fix runner class names in EntryPoint

Signed-off-by: André Silva <andre15andre@hotmail.com>

* Add JUnit4JacocoRunnerCoveredResultPerTestMethodTest

Signed-off-by: André Silva <andre15andre@hotmail.com>

* Fix CoverageFromClass not being serializable.

Because CoverageFromClass was not serializable, loading a serialized
CoverageDetailed object was not possible.

Signed-off-by: André Silva <andre15andre@hotmail.com>

* Update tests.

Signed-off-by: André Silva <andre15andre@hotmail.com>

* Add documentation

Signed-off-by: André Silva <andre15andre@hotmail.com>

* Fix flakyness

Signed-off-by: André Silva <andre15andre@hotmail.com>

* Add API documentation in the README

Signed-off-by: André Silva <andre15andre@hotmail.com>
  • Loading branch information
andre15silva committed Jun 24, 2021
1 parent 3541dd9 commit d7dd726
Show file tree
Hide file tree
Showing 32 changed files with 1,389 additions and 3 deletions.
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,23 @@ The output of all `runCoveragePerTestMethods()` API is a [`eu.stamp_project.test

* `Map<String, Coverage> getCoverageResultsMap()`: returns a map that associate the simple of a test method to its instruction coverage.
* `Coverage getCoverageOf(String testMethodName)`: returns the instruction coverage of a test method, specified by its simple name.


#### Covered results per test method

In the same way, you can have the covered results per test method using `runCoveredTestResultPerTestMethods()` API of `EntryPoint` class.

##### Output

The output of all `runCoveredTestResultPerTestMethods()` API is a [`eu.stamp_project.testrunner.listener.CoveredTestResultPerTestMethod`](https://github.com/STAMP-project/testrunner/blob/master/src/main/java/eu/stamp_project/testrunner/listener/CoveragePerTestMethod.java#L13).

* `Map<String, Coverage> getCoverageResultsMap()`: returns a map that associate the simple of a test method to its instruction coverage.
* `Coverage getCoverageOf(String testMethodName)`: returns the instruction coverage of a test method, specified by its simple name.
* `getRunningTests()`: returns the list of test methods that have been executed.
* `getPassingTests()`: returns the list of test methods that succeed.
* `getFailingTests()`: returns the list of test methods that failed.
* `getAssumptionFailingTests()`: returns the list of test methods that have a failing assumption. For example, in JUnit4 one can make assumptions using `org.junit.Assume` API, _e.g._ `Assume.assumeTrue(myBoolean)`. If the assumption does not hold, it is not necessary because the program is broken but rather than the test is irrelevant in the current state, _e.g._ one can make dedicated test to a platform.
* `getIgnoredTests()`: returns the list of test methods that are ignored.

#### Mutation Score

The test runner can now compute the mutation using [PIT](http://pitest.org).
Expand Down
90 changes: 90 additions & 0 deletions src/main/java/eu/stamp_project/testrunner/EntryPoint.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import eu.stamp_project.testrunner.listener.*;
import eu.stamp_project.testrunner.listener.impl.CoverageImpl;
import eu.stamp_project.testrunner.listener.impl.CoveragePerTestMethodImpl;
import eu.stamp_project.testrunner.listener.impl.CoveredTestResultPerTestMethodImpl;
import eu.stamp_project.testrunner.listener.impl.TestResultImpl;
import eu.stamp_project.testrunner.listener.junit4.JUnit4Coverage;
import eu.stamp_project.testrunner.listener.pit.AbstractParser;
Expand Down Expand Up @@ -420,6 +421,91 @@ public static CoveragePerTestMethod runCoveragePerTestMethods(String classpath,
return load;
}

/* COMPUTE COVERED TEST RESULT PER TEST METHOD API */

public static CoveredTestResultPerTestMethod runCoveredTestResultPerTestMethods(String classpath, String targetProjectClasses,
String fullQualifiedNameOfTestClass) throws TimeoutException {
return EntryPoint.runCoveredTestResultPerTestMethods(classpath, targetProjectClasses,
new String[]{fullQualifiedNameOfTestClass}, new String[0]);
}

public static CoveredTestResultPerTestMethod runCoveredTestResultPerTestMethods(String classpath, String targetProjectClasses,
String fullQualifiedNameOfTestClass, String testMethodName) throws TimeoutException {
return EntryPoint.runCoveredTestResultPerTestMethods(classpath, targetProjectClasses,
new String[]{fullQualifiedNameOfTestClass}, new String[]{testMethodName});
}

public static CoveredTestResultPerTestMethod runCoveredTestResultPerTestMethods(String classpath, String targetProjectClasses,
String fullQualifiedNameOfTestClass, String[] testMethodNames) throws TimeoutException {
return EntryPoint.runCoveredTestResultPerTestMethods(classpath, targetProjectClasses,
new String[]{fullQualifiedNameOfTestClass}, testMethodNames);
}

public static CoveredTestResultPerTestMethod runCoveredTestResultPerTestMethods(String classpath, String targetProjectClasses,
String[] fullQualifiedNameOfTestClasses) throws TimeoutException {
return EntryPoint.runCoveredTestResultPerTestMethods(classpath, targetProjectClasses, fullQualifiedNameOfTestClasses,
new String[0]);
}

/**
* Compute the test result and instruction coverage using <a
* href=http://www.eclemma.org/jacoco/>JaCoCo</a> for various test methods
* inside the given test classes.
* <p>
* This method computes the instruction coverage, using <a
* href=http://www.eclemma.org/jacoco/>JaCoCo</a> obtained by executing the
* given test methods inside the given test classes. This method requires the
* path to the binaries, i.e. .class, of the source code on which the
* instruction must be computed. This method computes the per test method
* coverage, <i>i.e.</i> the coverage obtained by each test methods, separately.
* It does not run one by one test methods, but rather use a specific
* implementation of {@link org.junit.runner.notification.RunListener}.
* </p>
*
* @param classpath the classpath required to run the given tests classes.
* @param targetProjectClasses path to the folder that contains binaries, i.e. .class, on which
* Jacoco computes the coverage.
* @param fullQualifiedNameOfTestClasses test classes to be run.
* @param methodNames test methods to be run.
* @return a Map that associate each test method name to its instruction
* coverage, as an instance of CoveredTestResultPerTestMethod {@link CoveredTestResultPerTestMethod} of
* test classes.
* @throws TimeoutException when the execution takes longer than timeoutInMs
*/
public static CoveredTestResultPerTestMethod runCoveredTestResultPerTestMethods(String classpath, String targetProjectClasses,
String[] fullQualifiedNameOfTestClasses, String[] methodNames) throws TimeoutException {
final String javaCommand = String.join(ConstantsHelper.WHITE_SPACE,
new String[]{
getJavaCommand(),
(classpath + ConstantsHelper.PATH_SEPARATOR + ABSOLUTE_PATH_TO_RUNNER_CLASSES
+ ConstantsHelper.PATH_SEPARATOR + ABSOLUTE_PATH_TO_JACOCO_DEPENDENCIES).replaceAll(" ", "%20"),
EntryPoint.jUnit5Mode ? EntryPoint.JUNIT5_JACOCO_RUNNER_COVERED_RESULT_PER_TEST_QUALIFIED_NAME : EntryPoint.JUNIT4_JACOCO_RUNNER_COVERED_RESULT_PER_TEST_QUALIFIED_NAME,
ParserOptions.FLAG_pathToCompiledClassesOfTheProject,
(targetProjectClasses).replaceAll(" ", "%20"), ParserOptions.FLAG_fullQualifiedNameOfTestClassToRun,
String.join(ConstantsHelper.PATH_SEPARATOR, fullQualifiedNameOfTestClasses),
methodNames.length == 0 ? "" : ParserOptions.FLAG_testMethodNamesToRun + ConstantsHelper.WHITE_SPACE +
String.join(ConstantsHelper.PATH_SEPARATOR, methodNames),
EntryPoint.blackList.isEmpty() ? ""
: (ParserOptions.FLAG_blackList + ConstantsHelper.WHITE_SPACE
+ String.join(ConstantsHelper.PATH_SEPARATOR, EntryPoint.blackList)),
EntryPoint.coverageDetail == ParserOptions.CoverageTransformerDetail.SUMMARIZED ? "" :
(ParserOptions.FLAG_coverage_detail + ConstantsHelper.WHITE_SPACE
+ EntryPoint.coverageDetail.name()),
});
try {
EntryPoint.runGivenCommandLine(javaCommand);
} catch (TimeoutException e) {
LOGGER.warn("Timeout when running {}", javaCommand);
throw e;
}
final CoveredTestResultPerTestMethod load = CoveredTestResultPerTestMethodImpl.load();
if (EntryPoint.verbose) {
LOGGER.info("Coverage per test methods has been computed {}{}", ConstantsHelper.LINE_SEPARATOR,
load.toString());
}
return load;
}

/* COMPUTE MUTATION SCORE WITH PIT API */

/**
Expand Down Expand Up @@ -575,6 +661,10 @@ private static void reset() {

private static final String JUNIT5_JACOCO_RUNNER_PER_TEST_QUALIFIED_NAME = "eu.stamp_project.testrunner.runner.coverage.JUnit5JacocoRunnerPerTestMethod";

private static final String JUNIT4_JACOCO_RUNNER_COVERED_RESULT_PER_TEST_QUALIFIED_NAME = "eu.stamp_project.testrunner.runner.coverage.JUnit4JacocoRunnerCoveredResultPerTestMethod";

private static final String JUNIT5_JACOCO_RUNNER_COVERED_RESULT_PER_TEST_QUALIFIED_NAME = "eu.stamp_project.testrunner.runner.coverage.JUnit5JacocoRunnerCoveredResultPerTestMethod";

private static final String ABSOLUTE_PATH_TO_RUNNER_CLASSES = initAbsolutePathToRunnerClasses();

private static final int DEFAULT_TIMEOUT = 10000;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package eu.stamp_project.testrunner.listener;

import eu.stamp_project.testrunner.utils.ConstantsHelper;

import java.io.Serializable;
import java.util.Map;

/**
* Stores both the individual test unit (i.e. method) results as well as the individual coverage
* information for each of them.
*
* @author andre15silva
*/
public interface CoveredTestResultPerTestMethod extends TestResult, Serializable {

public static final String SERIALIZE_NAME = "CoveredTestResultPerTest";

public static final String OUTPUT_DIR = "target" + ConstantsHelper.FILE_SEPARATOR;

public static final String EXTENSION = ".ser";

public Map<String, Coverage> getCoverageResultsMap();

public Coverage getCoverageOf(String testMethodName);

public void save();

}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public boolean isBetterThan(Coverage that) {

@Override
public String toString() {
return null;// return this.instructionsCovered + " / " + this.instructionsTotal;
return covered.toString();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package eu.stamp_project.testrunner.listener.impl;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

Expand All @@ -8,7 +9,9 @@
* @author Matias Martinez
*
*/
public class CoverageFromClass {
public class CoverageFromClass implements Serializable {

private static final long serialVersionUID = 4955729621536496728L;

private String classname;
private String packageName;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package eu.stamp_project.testrunner.listener.impl;

import eu.stamp_project.testrunner.listener.Coverage;
import eu.stamp_project.testrunner.listener.CoverageTransformer;
import eu.stamp_project.testrunner.listener.CoveredTestResultPerTestMethod;
import eu.stamp_project.testrunner.listener.TestResult;
import eu.stamp_project.testrunner.runner.Failure;
import eu.stamp_project.testrunner.runner.Loader;
import eu.stamp_project.testrunner.utils.ConstantsHelper;
import org.jacoco.core.data.ExecutionDataStore;
import org.jacoco.core.data.SessionInfoStore;
import org.jacoco.core.runtime.RuntimeData;

import java.io.File;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
* Default implementation of the CoveredTestResultPerTestMethod interface
*
* @author andre15silva
*/
public class CoveredTestResultPerTestMethodImpl implements CoveredTestResultPerTestMethod {

private static final long serialVersionUID = -789740001022671146L;

protected final Map<String, Coverage> coverageResultsMap;

protected final String classesDirectory;

protected transient RuntimeData data;

protected transient ExecutionDataStore executionData;

protected transient SessionInfoStore sessionInfos;

protected transient CoverageTransformer coverageTransformer;

private List<String> runningTests;
private List<Failure> failingTests;
private List<Failure> assumptionFailingTests;
private List<String> ignoredTests;

public CoveredTestResultPerTestMethodImpl(RuntimeData data, String classesDirectory, CoverageTransformer coverageTransformer) {
this.data = data;
this.classesDirectory = classesDirectory;
this.coverageResultsMap = new HashMap<>();
this.coverageTransformer = coverageTransformer;
this.runningTests = new ArrayList<>();
this.failingTests = new ArrayList<>();
this.assumptionFailingTests = new ArrayList<>();
this.ignoredTests = new ArrayList<>();

}

public String getClassesDirectory() {
return classesDirectory;
}

public RuntimeData getData() {
return data;
}

public ExecutionDataStore getExecutionData() {
return executionData;
}

public SessionInfoStore getSessionInfos() {
return sessionInfos;
}

public CoverageTransformer getCoverageTransformer() {
return coverageTransformer;
}

public void setData(RuntimeData data) {
this.data = data;
}

public void setExecutionData(ExecutionDataStore executionData) {
this.executionData = executionData;
}

public void setSessionInfos(SessionInfoStore sessionInfos) {
this.sessionInfos = sessionInfos;
}

@Override
public Map<String, Coverage> getCoverageResultsMap() {
return coverageResultsMap;
}

@Override
public Coverage getCoverageOf(String testMethodName) {
return this.getCoverageResultsMap().get(testMethodName);
}

@Override
public List<String> getRunningTests() {
return runningTests;
}

@Override
public List<String> getPassingTests() {
final List<String> failing = this.failingTests.stream()
.map(failure -> failure.testCaseName)
.collect(Collectors.toList());
final List<String> assumptionFailing = this.assumptionFailingTests.stream()
.map(failure -> failure.testCaseName)
.collect(Collectors.toList());
return this.runningTests.stream()
.filter(description -> !assumptionFailing.contains(description))
.filter(description -> !failing.contains(description))
.collect(Collectors.toList());
}

@Override
public TestResult aggregate(TestResult that) {
if (that instanceof CoveredTestResultPerTestMethodImpl) {
final CoveredTestResultPerTestMethodImpl thatListener = (CoveredTestResultPerTestMethodImpl) that;
this.runningTests.addAll(thatListener.runningTests);
this.failingTests.addAll(thatListener.failingTests);
this.assumptionFailingTests.addAll(thatListener.assumptionFailingTests);
this.ignoredTests.addAll(thatListener.ignoredTests);
}
return this;
}

@Override
public List<Failure> getFailingTests() {
return failingTests;
}

@Override
public List<Failure> getAssumptionFailingTests() {
return assumptionFailingTests;
}

@Override
public List<String> getIgnoredTests() {
return ignoredTests;
}

@Override
public Failure getFailureOf(String testMethodName) {
return this.getFailingTests().stream()
.filter(failure -> failure.testCaseName.equals(testMethodName))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(String.format("Could not find %s in failing test", testMethodName)));
}

@Override
public void save() {
File outputDir = new File(TestResult.OUTPUT_DIR);
if (!outputDir.exists()) {
if (!outputDir.mkdirs()) {
System.err.println("Error while creating output dir");
}
}
File f = new File(outputDir, SERIALIZE_NAME + EXTENSION);
try (FileOutputStream fout = new FileOutputStream(f)) {
try (ObjectOutputStream oos = new ObjectOutputStream(fout)) {
oos.writeObject(this);
} catch (Exception e) {
throw new RuntimeException(e);
}
} catch (Exception e) {
System.err.println("Error while writing serialized file.");
throw new RuntimeException(e);
}
System.out.println("File saved to the following path: " + f.getAbsolutePath());
}

/**
* Load from serialized object
*
* @return an Instance of CoveragePerTestMethod loaded from a serialized file.
*/
public static CoveredTestResultPerTestMethodImpl load() {
return new Loader<CoveredTestResultPerTestMethodImpl>().load(SERIALIZE_NAME);
}

@Override
public String toString() {
return "CoveredTestResultPerTestMethodImpl{" +
"coverageResultsMap=" + this.coverageResultsMap.keySet()
.stream()
.map(test -> "\t" + test + ": " + coverageResultsMap.get(test).toString())
.collect(Collectors.joining(ConstantsHelper.LINE_SEPARATOR)) +
", classesDirectory='" + classesDirectory + '\'' +
", data=" + data +
", executionData=" + executionData +
", sessionInfos=" + sessionInfos +
", coverageTransformer=" + coverageTransformer +
", runningTests=" + runningTests +
", failingTests=" + failingTests +
", assumptionFailingTests=" + assumptionFailingTests +
", ignoredTests=" + ignoredTests +
'}';
}

}
Loading

0 comments on commit d7dd726

Please sign in to comment.