diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/testimpact/CoverageToJsonStrategyBase.java b/agent/src/main/java/com/teamscale/jacoco/agent/testimpact/CoverageToJsonStrategyBase.java index 96280f102..70ec0dd0d 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/testimpact/CoverageToJsonStrategyBase.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/testimpact/CoverageToJsonStrategyBase.java @@ -121,7 +121,7 @@ private String createTestwiseCoverageReport(boolean partial) throws IOException, if (execution == null) { return null; } else { - return execution.getUniformPath(); + return execution.uniformPath; } }).collect(toList()); @@ -132,10 +132,9 @@ private String createTestwiseCoverageReport(boolean partial) throws IOException, reportGenerator.updateClassDirCache(); TestwiseCoverage testwiseCoverage = reportGenerator.convert(testExecFile); logger.debug("Created testwise coverage report (containing coverage for tests `{}`)", - testwiseCoverage.getTests().stream().map(TestCoverageBuilder::getUniformPath).collect(toList())); + testwiseCoverage.getTests().values().stream().map(TestCoverageBuilder::getUniformPath).collect(toList())); - TestwiseCoverageReport report = TestwiseCoverageReportBuilder - .createFrom(availableTests, testwiseCoverage.getTests(), testExecutions, partial); + TestwiseCoverageReport report = TestwiseCoverageReportBuilder.createFrom(availableTests, testwiseCoverage.getTests().values(), testExecutions, partial); testExecFile.delete(); testExecFile = null; diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/testimpact/CoverageViaHttpStrategy.java b/agent/src/main/java/com/teamscale/jacoco/agent/testimpact/CoverageViaHttpStrategy.java index 7651799f0..d40cf7687 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/testimpact/CoverageViaHttpStrategy.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/testimpact/CoverageViaHttpStrategy.java @@ -11,6 +11,8 @@ import com.teamscale.report.testwise.model.builder.TestInfoBuilder; import org.slf4j.Logger; +import java.util.Objects; + /** * Strategy which directly converts the collected coverage into a JSON object in place and returns the result to the * caller as response to the http request. If a test execution is given it is merged into the representation and @@ -36,12 +38,12 @@ public TestInfo testEnd(String test, TestExecution testExecution) TestInfoBuilder builder = new TestInfoBuilder(test); Dump dump = controller.dumpAndReset(); reportGenerator.updateClassDirCache(); - builder.setCoverage(reportGenerator.convert(dump)); + builder.setCoverage(Objects.requireNonNull(reportGenerator.convert(dump))); if (testExecution != null) { builder.setExecution(testExecution); } TestInfo testInfo = builder.build(); - logger.debug("Generated test info {}", testInfo.toString()); + logger.debug("Generated test info {}", testInfo); return testInfo; } } diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/testimpact/TestEventHandlerStrategyBase.java b/agent/src/main/java/com/teamscale/jacoco/agent/testimpact/TestEventHandlerStrategyBase.java index 840f5f7d6..cb8bad766 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/testimpact/TestEventHandlerStrategyBase.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/testimpact/TestEventHandlerStrategyBase.java @@ -65,7 +65,7 @@ public void testStart(String test) { public TestInfo testEnd(String test, TestExecution testExecution) throws JacocoRuntimeController.DumpException, CoverageGenerationException { if (testExecution != null) { - testExecution.setUniformPath(test); + testExecution.uniformPath = test; if (startTimestamp != -1) { long endTimestamp = System.currentTimeMillis(); testExecution.setDurationMillis(endTimestamp - startTimestamp); diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/upload/HttpZipUploaderBase.java b/agent/src/main/java/com/teamscale/jacoco/agent/upload/HttpZipUploaderBase.java index 52922f63e..4349e8533 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/upload/HttpZipUploaderBase.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/upload/HttpZipUploaderBase.java @@ -143,7 +143,7 @@ private File createZipFile(CoverageFile coverageFile) throws IOException { */ private void fillZipFile(ZipOutputStream zipOutputStream, CoverageFile coverageFile) throws IOException { zipOutputStream.putNextEntry(new ZipEntry(getZipEntryCoverageFileName(coverageFile))); - coverageFile.copy(zipOutputStream); + coverageFile.copyStream(zipOutputStream); for (Path additionalFile : additionalMetaDataFiles) { zipOutputStream.putNextEntry(new ZipEntry(additionalFile.getFileName().toString())); diff --git a/common-system-test/src/main/java/com/teamscale/test/commons/SystemTestUtils.java b/common-system-test/src/main/java/com/teamscale/test/commons/SystemTestUtils.java index a83f2316a..eebc26d52 100644 --- a/common-system-test/src/main/java/com/teamscale/test/commons/SystemTestUtils.java +++ b/common-system-test/src/main/java/com/teamscale/test/commons/SystemTestUtils.java @@ -49,7 +49,7 @@ public class SystemTestUtils { * Example: {@code file1.java:1,7-12;file2.java:9-22,33} */ public static String getCoverageString(TestInfo info) { - return info.paths.stream().flatMap(path -> path.getFiles().stream()) + return info.paths.stream().flatMap(path -> path.files.stream()) .map(file -> file.fileName + ":" + file.coveredLines).collect( Collectors.joining(";")); } diff --git a/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.java b/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.java index 9e4f78c63..e03b4a784 100644 --- a/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.java +++ b/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.java @@ -85,9 +85,9 @@ void testInteractionWithListenersAndCoverageApi() { assertThat(testExecutions).hasSize(2); assertThat(testExecutions).anySatisfy(testExecution -> - assertThat(testExecution.getUniformPath()).isEqualTo("MyClass/impactedTestCase()")); + assertThat(testExecution.uniformPath).isEqualTo("MyClass/impactedTestCase()")); assertThat(testExecutions).anySatisfy(testExecution -> - assertThat(testExecution.getUniformPath()).isEqualTo("MyClass/regularSkippedTestCase()")); + assertThat(testExecution.uniformPath).isEqualTo("MyClass/regularSkippedTestCase()")); } @Test @@ -125,6 +125,6 @@ void testSkipOfTestClass() { assertThat(testExecutions).hasSize(2); assertThat(testExecutions) - .allMatch(testExecution -> testExecution.getResult().equals(ETestExecutionResult.SKIPPED)); + .allMatch(testExecution -> testExecution.result.equals(ETestExecutionResult.SKIPPED)); } } \ No newline at end of file diff --git a/report-generator/build.gradle.kts b/report-generator/build.gradle.kts index 882e8782b..e9d3e4b12 100644 --- a/report-generator/build.gradle.kts +++ b/report-generator/build.gradle.kts @@ -3,6 +3,7 @@ plugins { com.teamscale.`java-convention` com.teamscale.coverage com.teamscale.publish + kotlin("jvm") } publishAs { diff --git a/report-generator/src/main/java/com/teamscale/report/EDuplicateClassFileBehavior.java b/report-generator/src/main/java/com/teamscale/report/EDuplicateClassFileBehavior.java deleted file mode 100644 index 9fb811ca8..000000000 --- a/report-generator/src/main/java/com/teamscale/report/EDuplicateClassFileBehavior.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.teamscale.report; - -/** - * Behavior when two non-identical class files with the same package name are found. - */ -public enum EDuplicateClassFileBehavior { - /** Completely ignores it. */ - IGNORE, - - /** Prints a warning to the logger. */ - WARN, - - /** Fails and stops further processing. */ - FAIL -} diff --git a/report-generator/src/main/java/com/teamscale/report/ReportUtils.java b/report-generator/src/main/java/com/teamscale/report/ReportUtils.java deleted file mode 100644 index c5513dd6f..000000000 --- a/report-generator/src/main/java/com/teamscale/report/ReportUtils.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.teamscale.report; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.teamscale.client.FileSystemUtils; -import com.teamscale.client.JsonUtils; -import com.teamscale.client.TestDetails; -import com.teamscale.report.testwise.ETestArtifactFormat; -import com.teamscale.report.testwise.model.TestExecution; -import com.teamscale.report.testwise.model.TestwiseCoverageReport; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -/** Utilities for generating reports. */ -public class ReportUtils { - - /** Converts to given test list to a json report and writes it to the given file. */ - public static void writeTestListReport(File reportFile, List report) throws IOException { - writeReportToFile(reportFile, report); - } - - /** Converts to given test execution report to a json report and writes it to the given file. */ - public static void writeTestExecutionReport(File reportFile, List report) throws IOException { - writeReportToFile(reportFile, report); - } - - /** Converts to given testwise coverage report to a json report and writes it to the given file. */ - public static void writeTestwiseCoverageReport(File reportFile, TestwiseCoverageReport report) throws IOException { - writeReportToFile(reportFile, report); - } - - /** Converts to given report to a json string. For testing only. */ - public static String getTestwiseCoverageReportAsString( - TestwiseCoverageReport report) throws JsonProcessingException { - return JsonUtils.serialize(report); - } - - /** Writes the report object to the given file as json. */ - private static void writeReportToFile(File reportFile, T report) throws IOException { - File directory = reportFile.getParentFile(); - if (!directory.isDirectory() && !directory.mkdirs()) { - throw new IOException("Failed to create directory " + directory.getAbsolutePath()); - } - JsonUtils.serializeToFile(reportFile, report); - } - - /** Recursively lists all files in the given directory that match the specified extension. */ - public static List readObjects(ETestArtifactFormat format, Class clazz, - List directoriesOrFiles) throws IOException { - List files = listFiles(format, directoriesOrFiles); - ArrayList result = new ArrayList<>(); - for (File file : files) { - T[] t = JsonUtils.deserializeFile(file, clazz); - if (t != null) { - result.addAll(Arrays.asList(t)); - } - } - return result; - } - - /** Recursively lists all files of the given artifact type. */ - public static List listFiles(ETestArtifactFormat format, List directoriesOrFiles) { - List filesWithSpecifiedArtifactType = new ArrayList<>(); - for (File directoryOrFile : directoriesOrFiles) { - if (directoryOrFile.isDirectory()) { - filesWithSpecifiedArtifactType.addAll(FileSystemUtils - .listFilesRecursively(directoryOrFile, file -> fileIsOfArtifactFormat(file, format))); - } else if (fileIsOfArtifactFormat(directoryOrFile, format)) { - filesWithSpecifiedArtifactType.add(directoryOrFile); - } - } - return filesWithSpecifiedArtifactType; - } - - private static boolean fileIsOfArtifactFormat(File file, ETestArtifactFormat format) { - return file.isFile() && - file.getName().startsWith(format.filePrefix) && - FileSystemUtils.getFileExtension(file).equalsIgnoreCase(format.extension); - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/jacoco/CoverageFile.java b/report-generator/src/main/java/com/teamscale/report/jacoco/CoverageFile.java deleted file mode 100644 index c121a013d..000000000 --- a/report-generator/src/main/java/com/teamscale/report/jacoco/CoverageFile.java +++ /dev/null @@ -1,133 +0,0 @@ -package com.teamscale.report.jacoco; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.file.Files; -import java.util.Objects; - -import com.teamscale.client.FileSystemUtils; - -import okhttp3.MultipartBody; -import okhttp3.RequestBody; - -/** - * Represents a coverage file on disk. The main purpose is to avoid reading the - * entire file into memory as this dramatically increases the memory footprint - * of the JVM which might run out of memory because of this. - * - * The object internally holds a counter of how many references to the file are - * currently held. This allows to share the same file for multiple uploads and - * deleting it once all uploads have succeeded. Use {@link #acquireReference()} - * to make the object aware that it was passed to another uploader and - * {@link #delete()} to signal that you no longer intend to access the file. - */ -public class CoverageFile { - - private final File coverageFile; - private int referenceCounter = 0; - - public CoverageFile(File coverageFile) { - this.coverageFile = coverageFile; - } - - /** - * Marks the file as being used by an additional uploader. This ensures that the - * file is not deleted until all users have signed via {@link #delete()} that - * they no longer intend to access the file. - */ - public CoverageFile acquireReference() { - referenceCounter++; - return this; - } - - /** - * Copies the coverage File in blocks from the disk to the output stream to - * avoid having to read the entire file into memory. - */ - public void copy(OutputStream outputStream) throws IOException { - FileInputStream inputStream = new FileInputStream(coverageFile); - FileSystemUtils.copy(inputStream, outputStream); - inputStream.close(); - } - - /** - * Get the filename of the coverage file on disk without its extension - */ - public String getNameWithoutExtension() { - return FileSystemUtils.getFilenameWithoutExtension(coverageFile); - } - - /** Get the filename of the coverage file. */ - public String getName() { - return coverageFile.getName(); - } - - /** - * Delete the coverage file from disk - */ - public void delete() throws IOException { - referenceCounter--; - if (referenceCounter <= 0) { - Files.delete(coverageFile.toPath()); - } - } - - /** - * Create a {@link okhttp3.MultipartBody} form body with the contents of the - * coverage file. - */ - public RequestBody createFormRequestBody() { - return RequestBody.create(MultipartBody.FORM, new File(coverageFile.getAbsolutePath())); - } - - /** - * Get the {@link java.io.OutputStream} in order to write to the coverage file. - * - * @throws IOException - * If the file did not exist yet and could not be created - */ - public OutputStream getOutputStream() throws IOException { - try { - return new FileOutputStream(coverageFile); - } catch (IOException e) { - throw new IOException("Could not create temporary coverage file" + this + ". " - + "This is used to cache the coverage file on disk before uploading it to its final destination. " - + "This coverage is lost. Please fix the underlying issue to avoid losing coverage.", e); - } - - } - - /** - * {@inheritDoc} - */ - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - CoverageFile that = (CoverageFile) o; - return coverageFile.equals(that.coverageFile); - } - - /** - * {@inheritDoc} - */ - @Override - public int hashCode() { - return Objects.hash(coverageFile); - } - - /** - * {@inheritDoc} - */ - @Override - public String toString() { - return coverageFile.getAbsolutePath(); - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/jacoco/EmptyReportException.java b/report-generator/src/main/java/com/teamscale/report/jacoco/EmptyReportException.java deleted file mode 100644 index 80f873def..000000000 --- a/report-generator/src/main/java/com/teamscale/report/jacoco/EmptyReportException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.teamscale.report.jacoco; - -/** - * Exception indicating that the generated report was empty and no {@link CoverageFile} was written to disk. - */ -public class EmptyReportException extends Exception { - - public EmptyReportException(String message) { - super(message); - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/jacoco/FilteringAnalyzer.java b/report-generator/src/main/java/com/teamscale/report/jacoco/FilteringAnalyzer.java deleted file mode 100644 index ab6607b58..000000000 --- a/report-generator/src/main/java/com/teamscale/report/jacoco/FilteringAnalyzer.java +++ /dev/null @@ -1,97 +0,0 @@ -/*-------------------------------------------------------------------------+ -| | -| Copyright (c) 2009-2018 CQSE GmbH | -| | -+-------------------------------------------------------------------------*/ -package com.teamscale.report.jacoco; - -import com.teamscale.report.util.BashFileSkippingInputStream; -import com.teamscale.report.util.ClasspathWildcardIncludeFilter; -import com.teamscale.report.util.ILogger; -import org.jacoco.core.analysis.Analyzer; -import org.jacoco.core.analysis.ICoverageVisitor; -import org.jacoco.core.data.ExecutionDataStore; - -import java.io.IOException; -import java.io.InputStream; -import java.util.function.Predicate; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -/** - * {@link Analyzer} that filters the analyzed class files based on a {@link Predicate}. - */ -/* package */ public class FilteringAnalyzer extends OpenAnalyzer { - - /** The filter for the analyzed class files. */ - private final ClasspathWildcardIncludeFilter locationIncludeFilter; - private final ILogger logger; - - public FilteringAnalyzer(ExecutionDataStore executionData, ICoverageVisitor coverageVisitor, - ClasspathWildcardIncludeFilter locationIncludeFilter, ILogger logger) { - super(executionData, coverageVisitor); - this.locationIncludeFilter = locationIncludeFilter; - this.logger = logger; - } - - /** {@inheritDoc} */ - @Override - public int analyzeAll(InputStream input, String location) throws IOException { - if (location.endsWith(".class") && !locationIncludeFilter.isIncluded(location)) { - logger.debug("Excluding class file " + location); - return 1; - } - if (location.endsWith(".jar")) { - return analyzeJar(input, location); - } - return super.analyzeAll(input, location); - } - - @Override - public void analyzeClass(final byte[] buffer, final String location) - throws IOException { - try { - analyzeClass(buffer); - } catch (final RuntimeException cause) { - if (isUnsupportedClassFile(cause)) { - logger.error(cause.getMessage() + " in " + location); - } else { - throw analyzerError(location, cause); - } - } - } - - /** - * Checks if the error indicates that the class file might be newer than what is currently supported by - * JaCoCo. The concrete error message seems to depend on the used JVM, so we only check for "Unsupported" which seems - * to be common amongst all of them. - */ - private boolean isUnsupportedClassFile(RuntimeException cause) { - return cause instanceof IllegalArgumentException && cause.getMessage() - .startsWith("Unsupported"); - } - - /** - * Copied from Analyzer.analyzeZip renamed to analyzeJar and added wrapping BashFileSkippingInputStream. - */ - protected int analyzeJar(final InputStream input, final String location) - throws IOException { - ZipInputStream zip = new ZipInputStream(new BashFileSkippingInputStream(input)); - ZipEntry entry; - int count = 0; - while ((entry = nextEntry(zip, location)) != null) { - count += analyzeAll(zip, location + "@" + entry.getName()); - } - return count; - } - - /** Copied from Analyzer.nextEntry. */ - private ZipEntry nextEntry(final ZipInputStream input, - final String location) throws IOException { - try { - return input.getNextEntry(); - } catch (final IOException e) { - throw analyzerError(location, e); - } - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/jacoco/JaCoCoXmlReportGenerator.java b/report-generator/src/main/java/com/teamscale/report/jacoco/JaCoCoXmlReportGenerator.java deleted file mode 100644 index 73ca6abb3..000000000 --- a/report-generator/src/main/java/com/teamscale/report/jacoco/JaCoCoXmlReportGenerator.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.teamscale.report.jacoco; - -import com.teamscale.report.EDuplicateClassFileBehavior; -import com.teamscale.report.jacoco.dump.Dump; -import com.teamscale.report.util.ClasspathWildcardIncludeFilter; -import com.teamscale.report.util.ILogger; -import org.jacoco.core.analysis.CoverageBuilder; -import org.jacoco.core.analysis.IBundleCoverage; -import org.jacoco.core.data.ExecutionDataStore; -import org.jacoco.core.data.SessionInfo; -import org.jacoco.report.IReportVisitor; -import org.jacoco.report.xml.XMLFormatter; - -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.util.Collections; -import java.util.List; - -/** Creates an XML report from binary execution data. */ -public class JaCoCoXmlReportGenerator { - - /** The logger. */ - private final ILogger logger; - - /** Directories and zip files that contain class files. */ - private final List codeDirectoriesOrArchives; - - /** - * Include filter to apply to all locations during class file traversal. - */ - private final ClasspathWildcardIncludeFilter locationIncludeFilter; - - /** Whether to ignore non-identical duplicates of class files. */ - private final EDuplicateClassFileBehavior duplicateClassFileBehavior; - - /** Whether to remove uncovered classes from the report. */ - private final boolean ignoreUncoveredClasses; - - /** Part of the error message logged when validating the coverage report fails. */ - private static final String MOST_LIKELY_CAUSE_MESSAGE = "Most likely you did not configure the agent correctly." + - " Please check that the includes and excludes options are set correctly so the relevant code is included." + - " If in doubt, first include more code and then iteratively narrow the patterns down to just the relevant code." + - " If you have specified the class-dir option, please make sure it points to a directory containing the" + - " class files/jars/wars/ears/etc. for which you are trying to measure code coverage."; - - public JaCoCoXmlReportGenerator(List codeDirectoriesOrArchives, - ClasspathWildcardIncludeFilter locationIncludeFilter, - EDuplicateClassFileBehavior duplicateClassFileBehavior, - boolean ignoreUncoveredClasses, ILogger logger) { - this.codeDirectoriesOrArchives = codeDirectoriesOrArchives; - this.duplicateClassFileBehavior = duplicateClassFileBehavior; - this.locationIncludeFilter = locationIncludeFilter; - this.ignoreUncoveredClasses = ignoreUncoveredClasses; - this.logger = logger; - } - - - /** - * Creates the report and writes it to a file. - * - * @return The file object of for the converted report or null if it could not be created - */ - public CoverageFile convert(Dump dump, File filePath) throws IOException, EmptyReportException { - CoverageFile coverageFile = new CoverageFile(filePath); - convertToReport(coverageFile, dump); - return coverageFile; - } - - /** Creates the report. */ - private void convertToReport(CoverageFile coverageFile, Dump dump) throws IOException, EmptyReportException { - ExecutionDataStore mergedStore = dump.store; - IBundleCoverage bundleCoverage = analyzeStructureAndAnnotateCoverage(mergedStore); - checkForEmptyReport(bundleCoverage); - try (OutputStream outputStream = coverageFile.getOutputStream()) { - createReport(outputStream, bundleCoverage, dump.info, mergedStore); - } - } - - private void checkForEmptyReport(IBundleCoverage coverage) throws EmptyReportException { - if (coverage.getPackages().size() == 0 || coverage.getLineCounter().getTotalCount() == 0) { - throw new EmptyReportException("The generated coverage report is empty. " + MOST_LIKELY_CAUSE_MESSAGE); - } - if (coverage.getLineCounter().getCoveredCount() == 0) { - throw new EmptyReportException( - "The generated coverage report does not contain any covered source code lines. " + - MOST_LIKELY_CAUSE_MESSAGE); - } - } - - /** Creates an XML report based on the given session and coverage data. */ - private static void createReport(OutputStream output, IBundleCoverage bundleCoverage, SessionInfo sessionInfo, - ExecutionDataStore store) throws IOException { - XMLFormatter xmlFormatter = new XMLFormatter(); - IReportVisitor visitor = xmlFormatter.createVisitor(output); - - visitor.visitInfo(Collections.singletonList(sessionInfo), store.getContents()); - visitor.visitBundle(bundleCoverage, null); - visitor.visitEnd(); - } - - /** - * Analyzes the structure of the class files in {@link #codeDirectoriesOrArchives} and builds an in-memory coverage - * report with the coverage in the given store. - */ - private IBundleCoverage analyzeStructureAndAnnotateCoverage(ExecutionDataStore store) throws IOException { - CoverageBuilder coverageBuilder = new TeamscaleCoverageBuilder(this.logger, - duplicateClassFileBehavior, ignoreUncoveredClasses); - - FilteringAnalyzer analyzer = new FilteringAnalyzer(store, coverageBuilder, locationIncludeFilter, logger); - - for (File file : codeDirectoriesOrArchives) { - analyzer.analyzeAll(file); - } - - return coverageBuilder.getBundle("dummybundle"); - } - -} diff --git a/report-generator/src/main/java/com/teamscale/report/jacoco/OpenAnalyzer.java b/report-generator/src/main/java/com/teamscale/report/jacoco/OpenAnalyzer.java index 97556dca8..7e679b05b 100644 --- a/report-generator/src/main/java/com/teamscale/report/jacoco/OpenAnalyzer.java +++ b/report-generator/src/main/java/com/teamscale/report/jacoco/OpenAnalyzer.java @@ -46,11 +46,11 @@ * - {@link #analyzeClass(byte[])} * - {@link #analyzerError(String, Exception)} *

- * When performing an update of JaCoCo we need to check that this file is still up-to-date. + * When performing an update of JaCoCo we need to check that this file is still up to date. *

* An {@link Analyzer} instance processes a set of Java class files and * calculates coverage data for them. For each class file the result is reported - * to a given {@link ICoverageVisitor} instance. In addition the + * to a given {@link ICoverageVisitor} instance. In addition, the * {@link Analyzer} requires a {@link ExecutionDataStore} instance that holds * the execution data for the classes to analyze. The {@link Analyzer} offers * several methods to analyze classes from a variety of sources. @@ -73,7 +73,7 @@ public class OpenAnalyzer { * class */ public OpenAnalyzer(final ExecutionDataStore executionData, - final ICoverageVisitor coverageVisitor) { + final ICoverageVisitor coverageVisitor) { this.executionData = executionData; this.coverageVisitor = coverageVisitor; this.stringPool = new StringPool(); @@ -203,17 +203,17 @@ public int analyzeAll(final InputStream input, final String location) throw analyzerError(location, e); } switch (detector.getType()) { - case ContentTypeDetector.CLASSFILE: - analyzeClass(detector.getInputStream(), location); - return 1; - case ContentTypeDetector.ZIPFILE: - return analyzeZip(detector.getInputStream(), location); - case ContentTypeDetector.GZFILE: - return analyzeGzip(detector.getInputStream(), location); - case ContentTypeDetector.PACK200FILE: - return analyzePack200(detector.getInputStream(), location); - default: - return 0; + case ContentTypeDetector.CLASSFILE: + analyzeClass(detector.getInputStream(), location); + return 1; + case ContentTypeDetector.ZIPFILE: + return analyzeZip(detector.getInputStream(), location); + case ContentTypeDetector.GZFILE: + return analyzeGzip(detector.getInputStream(), location); + case ContentTypeDetector.PACK200FILE: + return analyzePack200(detector.getInputStream(), location); + default: + return 0; } } @@ -312,4 +312,4 @@ private int analyzePack200(final InputStream input, final String location) return analyzeAll(unpackedInput, location); } -} +} \ No newline at end of file diff --git a/report-generator/src/main/java/com/teamscale/report/jacoco/TeamscaleCoverageBuilder.java b/report-generator/src/main/java/com/teamscale/report/jacoco/TeamscaleCoverageBuilder.java deleted file mode 100644 index 6cf802f5a..000000000 --- a/report-generator/src/main/java/com/teamscale/report/jacoco/TeamscaleCoverageBuilder.java +++ /dev/null @@ -1,76 +0,0 @@ -/*-------------------------------------------------------------------------+ -| | -| Copyright (c) 2009-2018 CQSE GmbH | -| | -+-------------------------------------------------------------------------*/ -package com.teamscale.report.jacoco; - -import com.teamscale.report.EDuplicateClassFileBehavior; -import com.teamscale.report.util.ILogger; -import org.jacoco.core.analysis.CoverageBuilder; -import org.jacoco.core.analysis.IBundleCoverage; -import org.jacoco.core.analysis.IClassCoverage; -import org.jacoco.core.analysis.ICounter; -import org.jacoco.core.internal.analysis.BundleCoverageImpl; - -import java.util.Collections; - -/** - * Modified {@link CoverageBuilder} can ignore non-identical duplicate classes or classes without coverage. In addition, - * coverage returned via {@link #getBundle(String)} will only return source file coverage because Teamscale does not - * need class coverage anyway. This reduces XML size by approximately half. - */ -/* package */class TeamscaleCoverageBuilder extends CoverageBuilder { - - /** The logger. */ - private final ILogger logger; - - /** How to behave if duplicate class files are encountered. */ - private final EDuplicateClassFileBehavior duplicateClassFileBehavior; - - /** Whether to ignore uncovered classes (i.e. leave them out of the report). */ - private final boolean ignoreUncoveredClasses; - - TeamscaleCoverageBuilder(ILogger logger, EDuplicateClassFileBehavior duplicateClassFileBehavior, - boolean removeUncoveredClasses) { - this.logger = logger; - this.duplicateClassFileBehavior = duplicateClassFileBehavior; - this.ignoreUncoveredClasses = removeUncoveredClasses; - } - - /** Just returns source file coverage, because Teamscale does not need class coverage. */ - @Override - public IBundleCoverage getBundle(final String name) { - return new BundleCoverageImpl(name, Collections.emptyList(), getSourceFiles()); - } - - /** {@inheritDoc} */ - @Override - public void visitCoverage(IClassCoverage coverage) { - if (ignoreUncoveredClasses && (coverage.getClassCounter().getStatus() & ICounter.FULLY_COVERED) == 0) { - return; - } - - try { - super.visitCoverage(coverage); - } catch (IllegalStateException e) { - switch (duplicateClassFileBehavior) { - case IGNORE: - return; - case WARN: - // we deliberately do not log the exception in this case as it does not provide any additional - // valuable information but confuses users into thinking there's a serious problem with the agent - // as they only see that there are stack traces in the log - logger.warn("Ignoring duplicate, non-identical class file for class " + coverage - .getName() + " compiled from source file " + coverage.getSourceFileName() + "." - + " This happens when a class with the same fully-qualified name is loaded twice but the two loaded class files are not identical." - + " A common reason for this is that the same library or shared code is included twice in your application but in two different versions." - + " The produced coverage for this class may not be accurate or may even be unusable." - + " To fix this problem, please resolve the conflict between both class files in your application."); - return; - default: - throw e; - } - } - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/jacoco/dump/Dump.java b/report-generator/src/main/java/com/teamscale/report/jacoco/dump/Dump.java deleted file mode 100644 index 09ad436f6..000000000 --- a/report-generator/src/main/java/com/teamscale/report/jacoco/dump/Dump.java +++ /dev/null @@ -1,26 +0,0 @@ -/*-------------------------------------------------------------------------+ -| | -| Copyright (c) 2009-2018 CQSE GmbH | -| | -+-------------------------------------------------------------------------*/ -package com.teamscale.report.jacoco.dump; - -import org.jacoco.core.data.ExecutionDataStore; -import org.jacoco.core.data.SessionInfo; - -/** All data received in one dump. */ -public class Dump { - - /** The session info. */ - public final SessionInfo info; - - /** The execution data store. */ - public final ExecutionDataStore store; - - /** Constructor. */ - public Dump(SessionInfo info, ExecutionDataStore store) { - this.info = info; - this.store = store; - } - -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/ETestArtifactFormat.java b/report-generator/src/main/java/com/teamscale/report/testwise/ETestArtifactFormat.java deleted file mode 100644 index 3ef64b3e2..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/ETestArtifactFormat.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.teamscale.report.testwise; - -/** Enum of test artifacts that can be converted to a full testwise coverage report later on. */ -public enum ETestArtifactFormat { - - /** A json list of tests ({@link com.teamscale.client.TestDetails}). */ - TEST_LIST("Test List", "test-list", "json"), - - /** A json list of test executions ({@link com.teamscale.report.testwise.model.TestExecution}). */ - TEST_EXECUTION("Test Execution", "test-execution", "json"), - - /** Binary jacoco test coverage (.exec file). */ - JACOCO("Jacoco", "", "exec"), - - /** Google closure coverage files with additional uniformPath entries. */ - CLOSURE("Closure Coverage", "closure-coverage", "json"); - - /** A readable name for the report type. */ - public final String readableName; - - /** Prefix to use when writing the report to the file system. */ - public final String filePrefix; - - /** File extension of the report. */ - public final String extension; - - ETestArtifactFormat(String readableName, String filePrefix, String extension) { - this.readableName = readableName; - this.filePrefix = filePrefix; - this.extension = extension; - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/TestwiseCoverageReportWriter.java b/report-generator/src/main/java/com/teamscale/report/testwise/TestwiseCoverageReportWriter.java deleted file mode 100644 index d43af754b..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/TestwiseCoverageReportWriter.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.teamscale.report.testwise; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; -import com.teamscale.client.JsonUtils; -import com.teamscale.client.StringUtils; -import com.teamscale.report.testwise.model.TestInfo; -import com.teamscale.report.testwise.model.builder.TestCoverageBuilder; -import com.teamscale.report.testwise.model.factory.TestInfoFactory; - -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.file.Files; -import java.util.function.Consumer; - -/** - * Writes out a {@link com.teamscale.report.testwise.model.TestwiseCoverageReport} one {@link TestInfo} after the other - * so that we do not need to keep them all in memory during the conversion. - */ -public class TestwiseCoverageReportWriter implements Consumer, AutoCloseable { - - /** Factory for converting {@link TestCoverageBuilder} objects to {@link TestInfo}s. */ - private final TestInfoFactory testInfoFactory; - - private final File outputFile; - /** After how many written tests a new file should be started. */ - private final int splitAfter; - - /** Writer instance to where the {@link com.teamscale.report.testwise.model.TestwiseCoverageReport} is written to. */ - private JsonGenerator jsonGenerator; - - /** Number of tests written to the file. */ - private int testsWritten = 0; - - /** Number of test files that have been written. */ - private int testFileCounter = 0; - - public TestwiseCoverageReportWriter(TestInfoFactory testInfoFactory, File outputFile, - int splitAfter) throws IOException { - this.testInfoFactory = testInfoFactory; - this.outputFile = outputFile; - this.splitAfter = splitAfter; - - startReport(); - } - - @Override - public void accept(TestCoverageBuilder testCoverageBuilder) { - TestInfo testInfo = testInfoFactory.createFor(testCoverageBuilder); - try { - writeTestInfo(testInfo); - } catch (IOException e) { - // Need to be wrapped in RuntimeException as Consumer does not allow to throw a checked Exception - throw new RuntimeException("Writing test info to report failed.", e); - } - } - - @Override - public void close() throws IOException { - for (TestInfo testInfo : testInfoFactory.createTestInfosWithoutCoverage()) { - writeTestInfo(testInfo); - } - endReport(); - } - - private void startReport() throws IOException { - testFileCounter++; - OutputStream outputStream = Files.newOutputStream(getOutputFile(testFileCounter).toPath()); - jsonGenerator = JsonUtils.createFactory().createGenerator(outputStream); - jsonGenerator.setPrettyPrinter(new DefaultPrettyPrinter()); - jsonGenerator.writeStartObject(); - jsonGenerator.writeFieldName("tests"); - jsonGenerator.writeStartArray(); - } - - private File getOutputFile(int testFileCounter) { - String name = this.outputFile.getName(); - name = StringUtils.stripSuffix(name, ".json"); - name = name + "-" + testFileCounter + ".json"; - return new File(this.outputFile.getParent(), name); - } - - private void writeTestInfo(TestInfo testInfo) throws IOException { - if (testsWritten >= splitAfter) { - endReport(); - testsWritten = 0; - startReport(); - } - jsonGenerator.writeObject(testInfo); - testsWritten++; - } - - private void endReport() throws IOException { - jsonGenerator.writeEndArray(); - jsonGenerator.writeEndObject(); - jsonGenerator.close(); - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/CachingExecutionDataReader.java b/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/CachingExecutionDataReader.java deleted file mode 100644 index e57d5b02b..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/CachingExecutionDataReader.java +++ /dev/null @@ -1,134 +0,0 @@ -package com.teamscale.report.testwise.jacoco; - -import com.teamscale.report.EDuplicateClassFileBehavior; -import com.teamscale.report.jacoco.dump.Dump; -import com.teamscale.report.testwise.jacoco.cache.AnalyzerCache; -import com.teamscale.report.testwise.jacoco.cache.CoverageGenerationException; -import com.teamscale.report.testwise.jacoco.cache.ProbesCache; -import com.teamscale.report.testwise.model.builder.TestCoverageBuilder; -import com.teamscale.report.util.ClasspathWildcardIncludeFilter; -import com.teamscale.report.util.ILogger; -import org.jacoco.core.data.ExecutionData; -import org.jacoco.core.data.ExecutionDataStore; - -import java.io.File; -import java.io.IOException; -import java.util.Collection; -import java.util.function.Consumer; -import java.util.stream.Collectors; - -/** - * Helper class for analyzing class files, reading execution data and converting them to coverage data. - */ -class CachingExecutionDataReader { - - private final ILogger logger; - private final Collection classesDirectories; - private final ClasspathWildcardIncludeFilter locationIncludeFilter; - private final EDuplicateClassFileBehavior duplicateClassFileBehavior; - private ProbesCache probesCache; - - public CachingExecutionDataReader(ILogger logger, Collection classesDirectories, - ClasspathWildcardIncludeFilter locationIncludeFilter, - EDuplicateClassFileBehavior duplicateClassFileBehavior) { - this.logger = logger; - this.classesDirectories = classesDirectories; - this.locationIncludeFilter = locationIncludeFilter; - this.duplicateClassFileBehavior = duplicateClassFileBehavior; - } - - /** - * Analyzes the class/jar/war/... files and creates a lookup of which probes belong to which method. - */ - public void analyzeClassDirs() { - if (probesCache == null) { - probesCache = new ProbesCache(logger, duplicateClassFileBehavior); - } - if (classesDirectories.isEmpty()) { - logger.warn("No class directories found for caching."); - return; - } - AnalyzerCache analyzer = new AnalyzerCache(probesCache, locationIncludeFilter, logger); - int classCount = 0; - for (File classDir : classesDirectories) { - if (classDir.exists()) { - try { - classCount += analyzer.analyzeAll(classDir); - } catch (IOException e) { - logger.error("Failed to analyze class files in " + classDir + "! " + - "Maybe the folder contains incompatible class files. " + - "Coverage for class files in this folder will be ignored.", e); - } - } - } - if (classCount == 0) { - String directoryList = classesDirectories.stream().map(File::getPath).collect(Collectors.joining(",")); - logger.error("No class files found in the given directories! " + directoryList); - } else if (probesCache.isEmpty()) { - String directoryList = classesDirectories.stream().map(File::getPath).collect(Collectors.joining(",")); - logger.error( - "None of the " + classCount + " class files found in the given directories match the configured include/exclude patterns! " + directoryList); - } - } - - /** - * Converts the given store to coverage data. The coverage will only contain line range coverage information. - */ - public DumpConsumer buildCoverageConsumer(ClasspathWildcardIncludeFilter locationIncludeFilter, - Consumer nextConsumer) { - return new DumpConsumer(logger, locationIncludeFilter, nextConsumer); - } - - /** - * Consumer of {@link Dump} objects. Converts them to {@link TestCoverageBuilder} and passes them to the - * nextConsumer. - */ - public class DumpConsumer implements Consumer { - - /** The logger. */ - private final ILogger logger; - - /** The location include filter to be applied on the profiled classes. */ - private final ClasspathWildcardIncludeFilter locationIncludeFilter; - - /** Consumer that should be called with the newly built TestCoverageBuilder. */ - private final Consumer nextConsumer; - - private DumpConsumer(ILogger logger, ClasspathWildcardIncludeFilter locationIncludeFilter, - Consumer nextConsumer) { - this.logger = logger; - this.locationIncludeFilter = locationIncludeFilter; - this.nextConsumer = nextConsumer; - } - - @Override - public void accept(Dump dump) { - String testId = dump.info.getId(); - if (testId.isEmpty()) { - // Ignore intermediate coverage that does not belong to any specific test - logger.debug("Found a session with empty name! This could indicate that coverage is dumped also for " + - "coverage in between tests or that the given test name was empty!"); - return; - } - try { - TestCoverageBuilder testCoverage = buildCoverage(testId, dump.store, locationIncludeFilter); - nextConsumer.accept(testCoverage); - } catch (CoverageGenerationException e) { - logger.error("Failed to generate coverage for test " + testId + "! Skipping to the next test.", e); - } - } - - /** - * Converts the given store to coverage data. The coverage will only contain line range coverage information. - */ - private TestCoverageBuilder buildCoverage(String testId, ExecutionDataStore executionDataStore, - ClasspathWildcardIncludeFilter locationIncludeFilter) throws CoverageGenerationException { - TestCoverageBuilder testCoverage = new TestCoverageBuilder(testId); - for (ExecutionData executionData : executionDataStore.getContents()) { - testCoverage.add(probesCache.getCoverage(executionData, locationIncludeFilter)); - } - probesCache.flushLogger(); - return testCoverage; - } - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/JaCoCoTestwiseReportGenerator.java b/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/JaCoCoTestwiseReportGenerator.java deleted file mode 100644 index 971557ca4..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/JaCoCoTestwiseReportGenerator.java +++ /dev/null @@ -1,142 +0,0 @@ -package com.teamscale.report.testwise.jacoco; - -import com.teamscale.report.EDuplicateClassFileBehavior; -import com.teamscale.report.jacoco.dump.Dump; -import com.teamscale.report.testwise.jacoco.cache.CoverageGenerationException; -import com.teamscale.report.testwise.model.TestwiseCoverage; -import com.teamscale.report.testwise.model.builder.TestCoverageBuilder; -import com.teamscale.report.util.ClasspathWildcardIncludeFilter; -import com.teamscale.report.util.ILogger; -import org.jacoco.core.data.ExecutionData; -import org.jacoco.core.data.ExecutionDataReader; -import org.jacoco.core.data.ExecutionDataStore; -import org.jacoco.core.data.IExecutionDataVisitor; -import org.jacoco.core.data.ISessionInfoVisitor; -import org.jacoco.core.data.SessionInfo; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.function.Consumer; - -/** - * Creates an XML report for an execution data store. The report is grouped by session. - *

- * The class files under test must be compiled with debug information otherwise no coverage will be collected. - */ -public class JaCoCoTestwiseReportGenerator { - - /** The execution data reader and converter. */ - private final CachingExecutionDataReader executionDataReader; - - /** The filter for the analyzed class files. */ - private final ClasspathWildcardIncludeFilter locationIncludeFilter; - - /** - * Create a new generator with a collection of class directories. - * - * @param codeDirectoriesOrArchives Root directory that contains the projects class files. - * @param locationIncludeFilter Filter for class files - * @param logger The logger - */ - public JaCoCoTestwiseReportGenerator(Collection codeDirectoriesOrArchives, - ClasspathWildcardIncludeFilter locationIncludeFilter, - EDuplicateClassFileBehavior duplicateClassFileBehavior, - ILogger logger) { - this.locationIncludeFilter = locationIncludeFilter; - this.executionDataReader = new CachingExecutionDataReader(logger, codeDirectoriesOrArchives, - locationIncludeFilter, duplicateClassFileBehavior); - updateClassDirCache(); - } - - /** - * Updates the probe cache of the {@link ExecutionDataReader}. - */ - public void updateClassDirCache() { - executionDataReader.analyzeClassDirs(); - } - - /** Converts the given dumps to a report. */ - public TestwiseCoverage convert(File executionDataFile) throws IOException, CoverageGenerationException { - TestwiseCoverage testwiseCoverage = new TestwiseCoverage(); - CachingExecutionDataReader.DumpConsumer dumpConsumer = executionDataReader - .buildCoverageConsumer(locationIncludeFilter, testwiseCoverage::add); - readAndConsumeDumps(executionDataFile, dumpConsumer); - return testwiseCoverage; - } - - /** Converts the given dump to a report. */ - public TestCoverageBuilder convert(Dump dump) throws CoverageGenerationException { - List list = new ArrayList<>(); - CachingExecutionDataReader.DumpConsumer dumpConsumer = executionDataReader - .buildCoverageConsumer(locationIncludeFilter, list::add); - dumpConsumer.accept(dump); - if (list.size() == 1) { - return list.get(0); - } else { - return null; - } - } - - /** Converts the given dumps to a report. */ - public void convertAndConsume(File executionDataFile, - Consumer consumer) throws IOException { - CachingExecutionDataReader.DumpConsumer dumpConsumer = executionDataReader - .buildCoverageConsumer(locationIncludeFilter, consumer); - readAndConsumeDumps(executionDataFile, dumpConsumer); - } - - /** Reads the dumps from the given *.exec file. */ - private void readAndConsumeDumps(File executionDataFile, Consumer dumpConsumer) throws IOException { - try (InputStream input = new BufferedInputStream(new FileInputStream(executionDataFile))) { - ExecutionDataReader executionDataReader = new ExecutionDataReader(input); - DumpCallback dumpCallback = new DumpCallback(dumpConsumer); - executionDataReader.setExecutionDataVisitor(dumpCallback); - executionDataReader.setSessionInfoVisitor(dumpCallback); - executionDataReader.read(); - // Ensure that the last read dump is also consumed - dumpCallback.processDump(); - } - } - - /** Collects execution information per session and passes it to the consumer . */ - private static class DumpCallback implements IExecutionDataVisitor, ISessionInfoVisitor { - - /** The dump that is currently being read. */ - private Dump currentDump = null; - - /** The store to which coverage is currently written to. */ - private ExecutionDataStore store; - - /** The consumer to pass {@link Dump}s to. */ - private final Consumer dumpConsumer; - - private DumpCallback(Consumer dumpConsumer) { - this.dumpConsumer = dumpConsumer; - } - - @Override - public void visitSessionInfo(SessionInfo info) { - processDump(); - store = new ExecutionDataStore(); - currentDump = new Dump(info, store); - } - - @Override - public void visitClassExecution(ExecutionData data) { - store.put(data); - } - - private void processDump() { - if (currentDump != null) { - dumpConsumer.accept(currentDump); - currentDump = null; - } - } - } -} \ No newline at end of file diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/AnalyzerCache.java b/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/AnalyzerCache.java deleted file mode 100644 index 6e67f2597..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/AnalyzerCache.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.teamscale.report.testwise.jacoco.cache; - -import com.teamscale.report.jacoco.FilteringAnalyzer; -import com.teamscale.report.util.ClasspathWildcardIncludeFilter; -import com.teamscale.report.util.ILogger; -import org.jacoco.core.analysis.Analyzer; -import org.jacoco.core.internal.analysis.CachingClassAnalyzer; -import org.jacoco.core.internal.analysis.ClassCoverageImpl; -import org.jacoco.core.internal.analysis.StringPool; -import org.jacoco.core.internal.data.CRC64; -import org.jacoco.core.internal.flow.ClassProbesAdapter; -import org.jacoco.core.internal.instr.InstrSupport; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Paths; - -/** - * An {@link AnalyzerCache} instance processes a set of Java class/jar/war/... files and builds a {@link - * ClassCoverageLookup} for each of the classes. - *

- * For every class that gets found {@link #analyzeClass(byte[])} is called. A class is identified by its class ID which - * is a CRC64 checksum of the classfile. We process each class with {@link CachingClassAnalyzer} to fill a {@link - * ClassCoverageLookup}. - */ -public class AnalyzerCache extends FilteringAnalyzer { - - /** The probes cache. */ - private final ProbesCache probesCache; - - - private final StringPool stringPool = new StringPool(); - - /** Creates a new analyzer filling the given cache. */ - public AnalyzerCache(ProbesCache probesCache, ClasspathWildcardIncludeFilter locationIncludeFilter, - ILogger logger) { - super(null, null, locationIncludeFilter, logger); - this.probesCache = probesCache; - } - - /** - * Analyses the given class. Instead of the original implementation in {@link Analyzer#analyzeClass(byte[])} we - * don't use concrete execution data, but instead build a probe cache to speed up repeated lookups. - */ - @Override - protected void analyzeClass(final byte[] source) { - long classId = CRC64.classId(source); - if (probesCache.containsClassId(classId)) { - return; - } - final ClassReader reader = InstrSupport.classReaderFor(source); - ClassCoverageLookup classCoverageLookup = probesCache.createClass(classId, reader.getClassName()); - - // Dummy class coverage object that allows us to subclass ClassAnalyzer with CachingClassAnalyzer and reuse its - // IFilterContext implementation - final ClassCoverageImpl dummyClassCoverage = new ClassCoverageImpl(reader.getClassName(), - classId, false); - - CachingClassAnalyzer classAnalyzer = new CachingClassAnalyzer(classCoverageLookup, dummyClassCoverage, - stringPool); - final ClassVisitor visitor = new ClassProbesAdapter(classAnalyzer, false); - reader.accept(visitor, 0); - } - - /** - * Adds caching for jar files to the analyze jar functionality. - */ - @Override - protected int analyzeJar(final InputStream input, final String location) throws IOException { - long jarId = CRC64.classId(Files.readAllBytes(Paths.get(location))); - int probesCountForJarId = probesCache.countForJarId(jarId); - if (probesCountForJarId != 0) { - return probesCountForJarId; - } - int count = super.analyzeJar(input, location); - probesCache.addJarId(jarId, count); - return count; - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/ClassCoverageLookup.java b/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/ClassCoverageLookup.java deleted file mode 100644 index 2675b3309..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/ClassCoverageLookup.java +++ /dev/null @@ -1,132 +0,0 @@ -package com.teamscale.report.testwise.jacoco.cache; - -import com.teamscale.client.StringUtils; -import com.teamscale.report.testwise.model.builder.FileCoverageBuilder; -import com.teamscale.report.util.ILogger; -import com.teamscale.report.util.SortedIntList; -import org.jacoco.core.data.ExecutionData; - -import java.util.ArrayList; -import java.util.List; - -/** - * Holds information about a class' probes and to which line ranges they refer. - *

- *

    - *
  • Create an instance of this class for every analyzed java class. - *
  • Set the file name of the java source file from which the class has been created. - *
  • Then call {@link #addProbe(int, SortedIntList)} for all probes and lines that belong to that probe. - *
  • Afterwards call {@link #getFileCoverage(ExecutionData, ILogger)} to transform probes ({@link - * ExecutionData}) for this class into covered lines ({@link FileCoverageBuilder}). - *
- */ -public class ClassCoverageLookup { - - /** Fully qualified name of the class (with / as separators). */ - private String className; - - /** Name of the java source file. */ - private String sourceFileName; - - /** - * Mapping from probe IDs to sets of covered lines. The index in this list corresponds to the probe ID. - */ - private final List probes = new ArrayList<>(); - - /** - * Constructor. - * - * @param className Classname as stored in the bytecode e.g. com/company/Example - */ - ClassCoverageLookup(String className) { - this.className = className; - } - - /** Sets the file name of the currently analyzed class (without path). */ - public void setSourceFileName(String sourceFileName) { - this.sourceFileName = sourceFileName; - } - - /** Adjusts the size of the probes list to the total probes count. */ - public void setTotalProbeCount(int count) { - ensureArraySize(count - 1); - } - - /** Adds the probe with the given id to the method. */ - public void addProbe(int probeId, SortedIntList lines) { - ensureArraySize(probeId); - probes.set(probeId, lines); - } - - /** - * Ensures that the probes list is big enough to allow access to the given index. Intermediate list entries are - * filled with null. - */ - private void ensureArraySize(int index) { - while (index >= probes.size()) { - probes.add(null); - } - } - - /** - * Generates {@link FileCoverageBuilder} from an {@link ExecutionData}. {@link ExecutionData} holds coverage of - * exactly one class (whereby inner classes are a separate class). This method returns a {@link FileCoverageBuilder} - * object which is later merged with the {@link FileCoverageBuilder} of other classes that reside in the same file. - */ - public FileCoverageBuilder getFileCoverage(ExecutionData executionData, - ILogger logger) throws CoverageGenerationException { - boolean[] executedProbes = executionData.getProbes(); - - if (checkProbeInvariant(executedProbes)) { - throw new CoverageGenerationException("Probe lookup does not match with actual probe size for " + - sourceFileName + " " + className + " (" + probes.size() + " vs " + executedProbes.length + ")! " + - "This is a bug in the profiler tooling. Please report it back to CQSE."); - } - if (sourceFileName == null) { - logger.warn( - "No source file name found for class " + className + "! This class was probably not compiled with " + - "debug information enabled!"); - return null; - } - - // we model the default package as the empty string - String packageName = ""; - if (className.contains("/")) { - packageName = StringUtils.removeLastPart(className, '/'); - } - final FileCoverageBuilder fileCoverage = new FileCoverageBuilder(packageName, sourceFileName); - fillFileCoverage(fileCoverage, executedProbes, logger); - - return fileCoverage; - } - - private void fillFileCoverage(FileCoverageBuilder fileCoverage, boolean[] executedProbes, ILogger logger) { - for (int i = 0; i < probes.size(); i++) { - SortedIntList coveredLines = probes.get(i); - if (!executedProbes[i]) { - continue; - } - // coveredLines is null if the probe is outside of a method - // Happens e.g. for methods generated by Lombok - if (coveredLines == null) { - logger.info(sourceFileName + " " + className + " did contain a covered probe " + i + "(of " + - executedProbes.length + ") that could not be " + - "matched to any method. This could be a bug in the profiler tooling. Please report it back " + - "to CQSE."); - continue; - } - if (coveredLines.isEmpty()) { - logger.debug( - sourceFileName + " " + className + " did contain a method with no line information. " + - "Does the class contain debug information?"); - continue; - } - fileCoverage.addLines(coveredLines); - } - } - - /** Checks that the executed probes is not smaller than the cached probes. */ - private boolean checkProbeInvariant(boolean[] executedProbes) { - return probes.size() > executedProbes.length; - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/ClassNotFoundLogger.java b/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/ClassNotFoundLogger.java deleted file mode 100644 index b8fe6ecc1..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/ClassNotFoundLogger.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.teamscale.report.testwise.jacoco.cache; - -import com.teamscale.report.util.ILogger; - -import java.util.HashSet; -import java.util.Set; - -/** - * Coordinates logging of missing class files to ensure the warnings are only emitted once and not for every individual - * test. - */ -/* package */ class ClassNotFoundLogger { - - /** The logger. */ - private final ILogger logger; - - /** Missing classes that will be logged when {@link #flush()} is called. */ - private final Set classesToBeLogged = new HashSet<>(); - - /** Classes that have already been reported as missing. */ - private final Set alreadyLoggedClasses = new HashSet<>(); - - /** Constructor */ - /* package */ ClassNotFoundLogger(ILogger logger) { - this.logger = logger; - } - - /** Saves the given class to be logged later on. Ensures that the class is only logged once. */ - /* package */ void log(String fullyQualifiedClassName) { - if (!alreadyLoggedClasses.contains(fullyQualifiedClassName)) { - classesToBeLogged.add(fullyQualifiedClassName); - } - } - - /** Writes a summary of the missing class files to the logger. */ - /* package */ void flush() { - if (classesToBeLogged.isEmpty()) { - return; - } - - logger.warn( - "Found coverage for " + classesToBeLogged - .size() + " classes that were not provided. Either you did not provide " + - "all relevant class files or you did not adjust the include/exclude filters on the agent to exclude " + - "coverage from irrelevant code. The classes are:" - ); - for (String fullyQualifiedClassName : classesToBeLogged) { - logger.warn(" - " + fullyQualifiedClassName); - } - alreadyLoggedClasses.addAll(classesToBeLogged); - classesToBeLogged.clear(); - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/CoverageGenerationException.java b/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/CoverageGenerationException.java deleted file mode 100644 index fa975c191..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/CoverageGenerationException.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.teamscale.report.testwise.jacoco.cache; - -/** - * Exception thrown during coverage generation. - */ -public class CoverageGenerationException extends Exception { - - /** Constructor. */ - public CoverageGenerationException(String message) { - super(message); - } - -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/ProbesCache.java b/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/ProbesCache.java deleted file mode 100644 index 603fc1337..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/ProbesCache.java +++ /dev/null @@ -1,113 +0,0 @@ -package com.teamscale.report.testwise.jacoco.cache; - -import com.teamscale.report.EDuplicateClassFileBehavior; -import com.teamscale.report.testwise.model.builder.FileCoverageBuilder; -import com.teamscale.report.util.ClasspathWildcardIncludeFilter; -import com.teamscale.report.util.ILogger; -import org.jacoco.core.data.ExecutionData; -import org.jacoco.report.JavaNames; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -/** - * Holds {@link ClassCoverageLookup}s for all analyzed classes. - */ -public class ProbesCache { - - /** The logger. */ - private final ILogger logger; - - /** A mapping from class ID (CRC64 of the class file) to {@link ClassCoverageLookup}. */ - private final HashMap classCoverageLookups = new HashMap<>(); - - /** Holds all fully-qualified class names that are already contained in the cache. */ - private final Set containedClasses = new HashSet<>(); - - private final Map containedJars = new HashMap<>(); - - /** Whether to ignore non-identical duplicates of class files. */ - private final EDuplicateClassFileBehavior duplicateClassFileBehavior; - - private final ClassNotFoundLogger classNotFoundLogger; - - /** Constructor. */ - public ProbesCache(ILogger logger, EDuplicateClassFileBehavior duplicateClassFileBehavior) { - this.logger = logger; - this.classNotFoundLogger = new ClassNotFoundLogger(logger); - this.duplicateClassFileBehavior = duplicateClassFileBehavior; - } - - /** Adds a new class entry to the cache and returns its {@link ClassCoverageLookup}. */ - public ClassCoverageLookup createClass(long classId, String className) { - if (containedClasses.contains(className)) { - if (duplicateClassFileBehavior != EDuplicateClassFileBehavior.IGNORE) { - logger.warn("Non-identical class file for class " + className + "." - + " This happens when a class with the same fully-qualified name is loaded twice but the two loaded class files are not identical." - + " A common reason for this is that the same library or shared code is included twice in your application but in two different versions." - + " The produced coverage for this class may not be accurate or may even be unusable." - + " To fix this problem, please resolve the conflict between both class files in your application."); - } - if (duplicateClassFileBehavior == EDuplicateClassFileBehavior.FAIL) { - throw new IllegalStateException( - "Found non-identical class file for class " + className + ". See logs for more details."); - } - } - containedClasses.add(className); - ClassCoverageLookup classCoverageLookup = new ClassCoverageLookup(className); - classCoverageLookups.put(classId, classCoverageLookup); - return classCoverageLookup; - } - - /** Returns whether a class with the given class ID has already been analyzed. */ - public boolean containsClassId(long classId) { - return classCoverageLookups.containsKey(classId); - } - - /** - * Returns the number of found class files in a cached jar file. Otherwise 0. - */ - public int countForJarId(long jarId) { - return containedJars.getOrDefault(jarId, 0); - } - - /** - * Adds a jar id along with the count of class files found in the jar. - */ - public void addJarId(long jarId, int count) { - containedJars.put(jarId, count); - } - - /** - * Converts the given {@link ExecutionData} to {@link FileCoverageBuilder} using the cached lookups or null if the - * class file of this class has not been included in the analysis or was not covered. - */ - public FileCoverageBuilder getCoverage(ExecutionData executionData, - ClasspathWildcardIncludeFilter locationIncludeFilter) throws CoverageGenerationException { - long classId = executionData.getId(); - if (!containsClassId(classId)) { - String fullyQualifiedClassName = new JavaNames().getQualifiedClassName(executionData.getName()); - if (locationIncludeFilter.isIncluded(fullyQualifiedClassName + ".class")) { - classNotFoundLogger.log(fullyQualifiedClassName); - } - return null; - } - if (!executionData.hasHits()) { - return null; - } - - return classCoverageLookups.get(classId).getFileCoverage(executionData, logger); - } - - /** Returns true if the cache does not contain coverage for any class. */ - public boolean isEmpty() { - return classCoverageLookups.isEmpty(); - } - - /** Prints a the collected class not found messages. */ - public void flushLogger() { - classNotFoundLogger.flush(); - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/ERevisionType.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/ERevisionType.java deleted file mode 100644 index 01417d1b5..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/ERevisionType.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.teamscale.report.testwise.model; - -/** Type of revision information. */ -public enum ERevisionType { - - /** Commit descriptor in the format branch:timestamp. */ - COMMIT, - - /** Source control revision, e.g. SVN revision or Git hash. */ - REVISION -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/FileCoverage.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/FileCoverage.java deleted file mode 100644 index 40ba4c93b..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/FileCoverage.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.teamscale.report.testwise.model; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -/** Holds coverage of a single file. */ -public class FileCoverage { - - /** The name of the file. */ - public final String fileName; - - /** A list of line ranges that have been covered. */ - public final String coveredLines; - - @JsonCreator - public FileCoverage(@JsonProperty("fileName") String fileName, @JsonProperty("coveredLines") String coveredLines) { - this.fileName = fileName; - this.coveredLines = coveredLines; - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/LineRange.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/LineRange.java deleted file mode 100644 index b280100b3..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/LineRange.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.teamscale.report.testwise.model; - -/** Holds a line range with start and end (both inclusive and 1-based). */ -public class LineRange { - - /** The start line (1-based). */ - private int start; - - /** The end line (1-based). */ - private int end; - - /** Constructor. */ - public LineRange(int start, int end) { - this.start = start; - this.end = end; - } - - /** @see #end */ - public int getEnd() { - return end; - } - - /** @see #end */ - public void setEnd(int end) { - this.end = end; - } - - /** - * Returns the line range as used in the XML report. - * A range is returned as e.g. 2-5 or simply 3 if the start and end are equal. - */ - public String toReportString() { - if (start == end) { - return String.valueOf(start); - } else { - return start + "-" + end; - } - } - - @Override - public String toString() { - return toReportString(); - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/PathCoverage.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/PathCoverage.java deleted file mode 100644 index 1886c7c9b..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/PathCoverage.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.teamscale.report.testwise.model; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.List; - -/** Container for {@link FileCoverage}s of the same path. */ -public class PathCoverage { - - /** File system path. */ - private final String path; - - /** Files with coverage. */ - private final List files; - - @JsonCreator - public PathCoverage(@JsonProperty("path") String path, @JsonProperty("files") List files) { - this.path = path; - this.files = files; - } - - public String getPath() { - return path; - } - - public List getFiles() { - return files; - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/RevisionInfo.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/RevisionInfo.java deleted file mode 100644 index b272ec1b6..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/RevisionInfo.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.teamscale.report.testwise.model; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.teamscale.client.CommitDescriptor; - -import java.io.Serializable; - -/** Revision information necessary for uploading reports to Teamscale. */ -public class RevisionInfo implements Serializable { - - private static final long serialVersionUID = 1L; - - /** The type of revision information. */ - private final ERevisionType type; - - /** The value. Either a commit descriptor or a source control revision, depending on {@link #type}. */ - private final String value; - - @JsonCreator - public RevisionInfo(@JsonProperty("type") ERevisionType type, @JsonProperty("value") String value) { - this.type = type; - this.value = value; - } - - /** Constructor for Commit. */ - public RevisionInfo(CommitDescriptor commit) { - type = ERevisionType.COMMIT; - value = commit.toString(); - } - - /** Constructor for Revision. */ - public RevisionInfo(String revision) { - type = ERevisionType.REVISION; - value = revision; - } - - /** - * Constructor in case you have both fields, and either may be null. If both are set, the commit wins. If both are - * null, {@link #type} will be {@link ERevisionType#REVISION} and {@link #value} will be null. - */ - public RevisionInfo(CommitDescriptor commit, String revision) { - if (commit == null) { - type = ERevisionType.REVISION; - value = revision; - } else { - type = ERevisionType.COMMIT; - value = commit.toString(); - } - } - - public ERevisionType getType() { - return type; - } - - public String getValue() { - return value; - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/TestExecution.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/TestExecution.java deleted file mode 100644 index 6f6409177..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/TestExecution.java +++ /dev/null @@ -1,129 +0,0 @@ -/*-------------------------------------------------------------------------+ -| | -| Copyright (c) 2005-2018 The ConQAT Project | -| | -| Licensed under the Apache License, Version 2.0 (the "License"); | -| you may not use this file except in compliance with the License. | -| You may obtain a copy of the License at | -| | -| http://www.apache.org/licenses/LICENSE-2.0 | -| | -| Unless required by applicable law or agreed to in writing, software | -| distributed under the License is distributed on an "AS IS" BASIS, | -| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | -| See the License for the specific language governing permissions and | -| limitations under the License. | -| | -+-------------------------------------------------------------------------*/ -package com.teamscale.report.testwise.model; - -import com.fasterxml.jackson.annotation.JsonAlias; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.io.Serializable; - -/** Representation of a single test (method) execution. */ -public class TestExecution implements Serializable { - - private static final long serialVersionUID = 1L; - - /** - * The uniform path of the test (method) that was executed. This is an absolute (i.e. hierarchical) reference which - * identifies the test uniquely in the scope of the Teamscale project. It may (but is not required to) correspond to - * the path of some automated test case source code known to Teamscale. If the test was parameterized, this path is - * expected to reflect the parameter in some manner. - */ - private String uniformPath; - - /** Duration of the execution in milliseconds. */ - @Deprecated - private long durationMillis; - - /** Duration of the execution in seconds. */ - @JsonProperty("duration") - @JsonAlias("durationSeconds") - private Double duration; - - /** The actual execution result state. */ - private ETestExecutionResult result; - - /** - * Optional message given for test failures (normally contains a stack trace). May be {@code null}. - */ - private String message; - - /** - * Needed for Jackson deserialization. - */ - @JsonCreator - public TestExecution() { - // Needed for Jackson - } - - public TestExecution(String name, long durationMillis, ETestExecutionResult result) { - this(name, durationMillis, result, null); - } - - public TestExecution(String name, long durationMillis, ETestExecutionResult result, String message) { - this.uniformPath = name; - this.durationMillis = durationMillis; - this.result = result; - this.message = message; - } - - /** @see #durationMillis */ - public double getDurationSeconds() { - if (duration != null) { - return duration; - } else { - return durationMillis / 1000.0; - } - } - - /** @see #result */ - public ETestExecutionResult getResult() { - return result; - } - - /** @see #message */ - public String getMessage() { - return message; - } - - /** @see #uniformPath */ - public String getUniformPath() { - return uniformPath; - } - - /** @see #uniformPath */ - public void setUniformPath(String uniformPath) { - this.uniformPath = uniformPath; - } - - /** @see #durationMillis */ - public void setDurationMillis(long durationMillis) { - this.durationMillis = durationMillis; - } - - /** @see #result */ - public void setResult(ETestExecutionResult result) { - this.result = result; - } - - /** @see #message */ - public void setMessage(String message) { - this.message = message; - } - - @Override - public String toString() { - return "TestExecution{" + - "uniformPath='" + uniformPath + '\'' + - ", durationMillis=" + durationMillis + - ", duration=" + duration + - ", result=" + result + - ", message='" + message + '\'' + - '}'; - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/TestInfo.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/TestInfo.java deleted file mode 100644 index 8648662ec..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/TestInfo.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.teamscale.report.testwise.model; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.ArrayList; -import java.util.List; - -/** Generic container of all information about a specific test as written to the report. */ -@SuppressWarnings({"FieldCanBeLocal", "WeakerAccess"}) -public class TestInfo { - - /** Unique name of the test case by using a path like hierarchical description, which can be shown in the UI. */ - public final String uniformPath; - - /** - * Path to the source of the method. Will be equal to uniformPath in most cases, but e.g. @Test methods in a Base - * class will have the sourcePath pointing to the Base class which contains the actual implementation whereas - * uniformPath will contain the class name of the most specific subclass, from where it was actually executed. - */ - public final String sourcePath; - - /** - * Some kind of content to tell whether the test specification has changed. Can be revision number or hash over the - * specification or similar. - */ - public final String content; - - /** Duration of the execution in seconds. */ - public final Double duration; - - /** The actual execution result state. */ - public final ETestExecutionResult result; - - /** - * Optional message given for test failures (normally contains a stack trace). May be {@code null}. - */ - public final String message; - - /** All paths that the test did cover. */ - public final List paths = new ArrayList<>(); - - @JsonCreator - public TestInfo(@JsonProperty("uniformPath") String uniformPath, @JsonProperty("sourcePath") String sourcePath, - @JsonProperty("content") String content, @JsonProperty("duration") Double duration, - @JsonProperty("result") ETestExecutionResult result, - @JsonProperty("message") String message) { - this.uniformPath = uniformPath; - this.sourcePath = sourcePath; - this.content = content; - this.duration = duration; - this.result = result; - this.message = message; - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/TestwiseCoverage.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/TestwiseCoverage.java deleted file mode 100644 index 94e786a44..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/TestwiseCoverage.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.teamscale.report.testwise.model; - -import com.teamscale.report.testwise.model.builder.TestCoverageBuilder; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -/** Container for coverage produced by multiple tests. */ -public class TestwiseCoverage { - - /** A mapping from test ID to {@link TestCoverageBuilder}. */ - private final Map tests = new HashMap<>(); - - /** - * Adds the {@link TestCoverageBuilder} to the map. - * If there is already a test with the same ID the coverage is merged. - */ - public void add(TestCoverageBuilder coverage) { - if (coverage == null || coverage.isEmpty()) { - return; - } - if (tests.containsKey(coverage.getUniformPath())) { - TestCoverageBuilder testCoverage = tests.get(coverage.getUniformPath()); - testCoverage.addAll(coverage.getFiles()); - } else { - tests.put(coverage.getUniformPath(), coverage); - } - } - - /** - * Merges the given {@link TestwiseCoverage} with this one. - */ - public void add(TestwiseCoverage testwiseCoverage) { - if (testwiseCoverage == null) { - return; - } - for (TestCoverageBuilder value : testwiseCoverage.tests.values()) { - this.add(value); - } - } - - public Collection getTests() { - return tests.values(); - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/TestwiseCoverageReport.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/TestwiseCoverageReport.java deleted file mode 100644 index 1ec2f2e73..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/TestwiseCoverageReport.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.teamscale.report.testwise.model; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.ArrayList; -import java.util.List; - -/** Container for coverage produced by multiple tests. */ -public class TestwiseCoverageReport { - - /** - * If set to `true` the set of tests contained in the report don't represent the full set of tests within a - * partition. These tests are added or updated in Teamscale, but no tests or executable units that are missing in - * the report will be deleted. - */ - public final boolean partial; - - /** The tests contained in the report. */ - public final List tests = new ArrayList<>(); - - @JsonCreator - public TestwiseCoverageReport(@JsonProperty("partial") boolean partial) { - this.partial = partial; - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/FileCoverageBuilder.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/FileCoverageBuilder.java deleted file mode 100644 index 0782f89d4..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/FileCoverageBuilder.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.teamscale.report.testwise.model.builder; - -import com.teamscale.report.testwise.model.FileCoverage; -import com.teamscale.report.testwise.model.LineRange; -import com.teamscale.report.util.SortedIntList; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -/** Holds coverage of a single file. */ -public class FileCoverageBuilder { - - /** The file system path of the file not including the file itself. */ - private final String path; - - /** The name of the file. */ - private final String fileName; - - /** - * A list of line numbers that have been covered. Using a set here is too memory intensive. - */ - private final SortedIntList coveredLines = new SortedIntList(); - - /** Constructor. */ - public FileCoverageBuilder(String path, String fileName) { - this.path = path; - this.fileName = fileName; - } - - /** @see #fileName */ - public String getFileName() { - return fileName; - } - - /** @see #path */ - public String getPath() { - return path; - } - - /** Adds a line as covered. */ - public void addLine(int line) { - coveredLines.add(line); - } - - /** Adds a line range as covered. */ - public void addLineRange(int start, int end) { - for (int i = start; i <= end; i++) { - coveredLines.add(i); - } - } - - /** Adds set of lines as covered. */ - public void addLines(SortedIntList range) { - coveredLines.addAll(range); - } - - /** Merges the list of ranges into the current list. */ - public void merge(FileCoverageBuilder other) { - if (!other.fileName.equals(fileName) || !other.path.equals(path)) { - throw new AssertionError("Cannot merge coverage of two different files! This is a bug!"); - } - coveredLines.addAll(other.coveredLines); - } - - /** - * Merges all neighboring line numbers to ranges. E.g. a list of [[1-5],[3-7],[8-10],[12-14]] becomes - * [[1-10],[12-14]] - */ - public static List compactifyToRanges(SortedIntList lines) { - if (lines.size() == 0) { - return new ArrayList<>(); - } - - int firstLine = lines.get(0); - LineRange currentRange = new LineRange(firstLine, firstLine); - - List compactifiedRanges = new ArrayList<>(); - compactifiedRanges.add(currentRange); - - for (int i = 0; i < lines.size(); i++) { - int currentLine = lines.get(i); - if (currentRange.getEnd() == currentLine || currentRange.getEnd() == currentLine - 1) { - currentRange.setEnd(currentLine); - } else { - currentRange = new LineRange(currentLine, currentLine); - compactifiedRanges.add(currentRange); - } - } - return compactifiedRanges; - } - - /** - * Returns a compact string representation of the covered lines. Continuous line ranges are merged to ranges and - * sorted. Individual ranges are separated by commas. E.g. 1-5,7,9-11. - */ - public String computeCompactifiedRangesAsString() { - List coveredRanges = compactifyToRanges(coveredLines); - return coveredRanges.stream().map(LineRange::toReportString).collect(Collectors.joining(",")); - } - - /** Returns true if there is no coverage for the file yet. */ - public boolean isEmpty() { - return coveredLines.size() == 0; - } - - /** Builds the {@link FileCoverage} object, which is serialized into the report. */ - public FileCoverage build() { - return new FileCoverage(fileName, computeCompactifiedRangesAsString()); - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/PathCoverageBuilder.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/PathCoverageBuilder.java deleted file mode 100644 index 8da3d78c3..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/PathCoverageBuilder.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.teamscale.report.testwise.model.builder; - -import com.teamscale.report.testwise.model.FileCoverage; -import com.teamscale.report.testwise.model.PathCoverage; - -import java.util.Collection; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static java.util.stream.Collectors.toList; - -/** Container for {@link FileCoverageBuilder}s of the same path. */ -public class PathCoverageBuilder { - - /** File system path. */ - private final String path; - - /** Mapping from file names to {@link FileCoverageBuilder}. */ - private final Map fileCoverageList = new HashMap<>(); - - /** Constructor. */ - public PathCoverageBuilder(String path) { - this.path = path; - } - - /** @see #path */ - public String getPath() { - return path; - } - - /** - * Adds the given {@link FileCoverageBuilder} to the container. - * If coverage for the same file already exists it gets merged. - */ - public void add(FileCoverageBuilder fileCoverage) { - if (fileCoverageList.containsKey(fileCoverage.getFileName())) { - FileCoverageBuilder existingFile = fileCoverageList.get(fileCoverage.getFileName()); - existingFile.merge(fileCoverage); - } else { - fileCoverageList.put(fileCoverage.getFileName(), fileCoverage); - } - } - - /** Returns a collection of {@link FileCoverageBuilder}s associated with this path. */ - public Collection getFiles() { - return fileCoverageList.values(); - } - - /** Builds a {@link PathCoverage} object. */ - public PathCoverage build() { - List files = fileCoverageList.values().stream() - .sorted(Comparator.comparing(FileCoverageBuilder::getFileName)) - .map(FileCoverageBuilder::build).collect(toList()); - return new PathCoverage(path, files); - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/TestCoverageBuilder.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/TestCoverageBuilder.java deleted file mode 100644 index b8eedce72..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/TestCoverageBuilder.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.teamscale.report.testwise.model.builder; - -import com.teamscale.report.testwise.model.PathCoverage; - -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static java.util.stream.Collectors.toList; - -/** Generic holder of test coverage of a single test based on line-ranges. */ -public class TestCoverageBuilder { - - /** The uniformPath of the test (see TEST_IMPACT_ANALYSIS_DOC.md for more information). */ - private final String uniformPath; - - /** Mapping from path names to all files on this path. */ - private final Map pathCoverageList = new HashMap<>(); - - /** Constructor. */ - public TestCoverageBuilder(String uniformPath) { - this.uniformPath = uniformPath; - } - - /** @see #uniformPath */ - public String getUniformPath() { - return uniformPath; - } - - /** Returns a collection of {@link PathCoverageBuilder}s associated with the test. */ - public List getPaths() { - return pathCoverageList.values().stream().sorted(Comparator.comparing(PathCoverageBuilder::getPath)) - .map(PathCoverageBuilder::build).collect(toList()); - } - - /** Adds the {@link FileCoverageBuilder} to into the map, but filters out file coverage that is null or empty. */ - public void add(FileCoverageBuilder fileCoverage) { - if (fileCoverage == null || fileCoverage.isEmpty() - || fileCoverage.getFileName() == null || fileCoverage.getPath() == null) { - return; - } - PathCoverageBuilder pathCoverage = pathCoverageList - .computeIfAbsent(fileCoverage.getPath(), PathCoverageBuilder::new); - pathCoverage.add(fileCoverage); - } - - /** Adds the {@link FileCoverageBuilder}s into the map, but filters out empty ones. */ - public void addAll(List fileCoverageList) { - for (FileCoverageBuilder fileCoverage : fileCoverageList) { - add(fileCoverage); - } - } - - /** Returns all {@link FileCoverageBuilder}s stored for the test. */ - public List getFiles() { - return pathCoverageList.values().stream() - .flatMap(path -> path.getFiles().stream()) - .collect(toList()); - } - - /** Returns true if there is no coverage for the test yet. */ - public boolean isEmpty() { - return pathCoverageList.isEmpty(); - } - -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/TestInfoBuilder.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/TestInfoBuilder.java deleted file mode 100644 index 6a4bd08c0..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/TestInfoBuilder.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.teamscale.report.testwise.model.builder; - -import com.teamscale.client.TestDetails; -import com.teamscale.report.testwise.model.ETestExecutionResult; -import com.teamscale.report.testwise.model.TestExecution; -import com.teamscale.report.testwise.model.TestInfo; - -/** Generic container of all information about a specific test including details, execution info and coverage. */ -public class TestInfoBuilder { - - /** Unique name of the test case by using a path like hierarchical description, which can be shown in the UI. */ - public final String uniformPath; - - /** - * Path to the source of the method. Will be equal to uniformPath in most cases, but e.g. @Test methods in a base - * class will have the sourcePath pointing to the base class which contains the actual implementation whereas - * uniformPath will contain the the class name of the most specific subclass, from where it was actually executed. - */ - private String sourcePath = null; - - /** - * Some kind of content to tell whether the test specification has changed. Can be revision number or hash over the - * specification or similar. - */ - private String content = null; - - /** Duration of the execution in milliseconds. */ - private Double durationSeconds = null; - - /** The actual execution result state. */ - private ETestExecutionResult result; - - /** - * Optional message given for test failures (normally contains a stack trace). May be {@code null}. - */ - private String message; - - /** Coverage generated by this test. */ - private TestCoverageBuilder coverage; - - /** Constructor. */ - /* package */ - public TestInfoBuilder(String uniformPath) { - this.uniformPath = uniformPath; - } - - /** @see #uniformPath */ - public String getUniformPath() { - return uniformPath; - } - - /** Returns true if there is no coverage for the test yet. */ - public boolean isEmpty() { - return coverage.isEmpty(); - } - - /** Sets the test details fields. */ - public void setDetails(TestDetails details) { - if (details != null) { - sourcePath = details.sourcePath; - content = details.content; - } - } - - /** Sets the test execution fields. */ - public void setExecution(TestExecution execution) { - if (execution != null) { - durationSeconds = execution.getDurationSeconds(); - result = execution.getResult(); - message = execution.getMessage(); - } - } - - /** @see #coverage */ - public void setCoverage(TestCoverageBuilder coverage) { - this.coverage = coverage; - } - - /** Builds a {@link TestInfo} object of the data in this container. */ - public TestInfo build() { - TestInfo testInfo = new TestInfo(uniformPath, sourcePath, content, durationSeconds, result, message); - if (coverage != null) { - testInfo.paths.addAll(coverage.getPaths()); - } - return testInfo; - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/TestwiseCoverageReportBuilder.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/TestwiseCoverageReportBuilder.java deleted file mode 100644 index ae221bc7d..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/TestwiseCoverageReportBuilder.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.teamscale.report.testwise.model.builder; - -import com.teamscale.client.TestDetails; -import com.teamscale.report.testwise.model.TestExecution; -import com.teamscale.report.testwise.model.TestInfo; -import com.teamscale.report.testwise.model.TestwiseCoverageReport; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** Container for coverage produced by multiple tests. */ -public class TestwiseCoverageReportBuilder { - - /** A mapping from test ID to {@link TestCoverageBuilder}. */ - private final Map tests = new HashMap<>(); - - /** - * Adds the {@link TestCoverageBuilder} to the map. If there is already a test with the same ID the coverage is - * merged. - */ - public static TestwiseCoverageReport createFrom( - Collection testDetailsList, - Collection testCoverage, - Collection testExecutions, - boolean partial - ) { - TestwiseCoverageReportBuilder report = new TestwiseCoverageReportBuilder(); - for (TestDetails testDetails : testDetailsList) { - TestInfoBuilder container = new TestInfoBuilder(testDetails.uniformPath); - container.setDetails(testDetails); - report.tests.put(testDetails.uniformPath, container); - } - for (TestCoverageBuilder coverage : testCoverage) { - TestInfoBuilder container = resolveUniformPath(report, coverage.getUniformPath()); - if (container == null) { - continue; - } - container.setCoverage(coverage); - } - for (TestExecution testExecution : testExecutions) { - TestInfoBuilder container = resolveUniformPath(report, testExecution.getUniformPath()); - if (container == null) { - continue; - } - container.setExecution(testExecution); - } - return report.build(partial); - } - - private static TestInfoBuilder resolveUniformPath(TestwiseCoverageReportBuilder report, String uniformPath) { - TestInfoBuilder container = report.tests.get(uniformPath); - if (container != null) { - return container; - } - String shortenedUniformPath = stripParameterizedTestArguments(uniformPath); - TestInfoBuilder testInfoBuilder = report.tests.get(shortenedUniformPath); - if (testInfoBuilder == null) { - System.err.println("No container found for test '" + uniformPath + "'!"); - } - return testInfoBuilder; - } - - /** - * Removes parameterized test arguments from the given uniform path. - */ - public static String stripParameterizedTestArguments(String uniformPath) { - return uniformPath.replaceFirst("(.*\\))\\[.*]", "$1"); - } - - private TestwiseCoverageReport build(boolean partial) { - TestwiseCoverageReport report = new TestwiseCoverageReport(partial); - List testInfoBuilders = new ArrayList<>(tests.values()); - testInfoBuilders.sort(Comparator.comparing(TestInfoBuilder::getUniformPath)); - for (TestInfoBuilder testInfoBuilder : testInfoBuilders) { - TestInfo testInfo = testInfoBuilder.build(); - if (testInfo == null) { - System.err.println("No coverage for test '" + testInfoBuilder.getUniformPath() + "'"); - continue; - } - report.tests.add(testInfo); - } - return report; - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/factory/TestInfoFactory.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/factory/TestInfoFactory.java deleted file mode 100644 index 0895630a9..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/factory/TestInfoFactory.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.teamscale.report.testwise.model.factory; - -import com.teamscale.client.TestDetails; -import com.teamscale.report.testwise.model.TestExecution; -import com.teamscale.report.testwise.model.TestInfo; -import com.teamscale.report.testwise.model.builder.TestCoverageBuilder; -import com.teamscale.report.testwise.model.builder.TestInfoBuilder; -import com.teamscale.report.testwise.model.builder.TestwiseCoverageReportBuilder; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Factory class for converting {@link TestCoverageBuilder} to {@link TestInfo}s while augmenting them with information - * from test details and test executions. - */ -public class TestInfoFactory { - - /** Maps uniform paths to test details. */ - private Map testDetailsMap = new HashMap<>(); - - /** Maps uniform paths to test executions. */ - private Map testExecutionsMap = new HashMap<>(); - - /** Holds all uniform paths for tests that have been written to the outputFile. */ - private final Set processedTestUniformPaths = new HashSet<>(); - - public TestInfoFactory(List testDetails, List testExecutions) { - for (TestDetails testDetail : testDetails) { - testDetailsMap.put(testDetail.uniformPath, testDetail); - } - for (TestExecution testExecution : testExecutions) { - testExecutionsMap.put(testExecution.getUniformPath(), testExecution); - } - } - - /** - * Converts the given {@link TestCoverageBuilder} to a {@link TestInfo} using the internally stored test details and - * test executions. - */ - public TestInfo createFor(TestCoverageBuilder testCoverageBuilder) { - String resolvedUniformPath = resolveUniformPath(testCoverageBuilder.getUniformPath()); - processedTestUniformPaths.add(resolvedUniformPath); - - TestInfoBuilder container = new TestInfoBuilder(resolvedUniformPath); - container.setCoverage(testCoverageBuilder); - TestDetails testDetails = testDetailsMap.get(resolvedUniformPath); - if (testDetails == null) { - System.err.println("No test details found for " + resolvedUniformPath); - } - container.setDetails(testDetails); - TestExecution execution = testExecutionsMap.get(resolvedUniformPath); - if (execution == null) { - System.err.println("No test execution found for " + resolvedUniformPath); - } - container.setExecution(execution); - return container.build(); - } - - /** Returns {@link TestInfo}s for all tests that have not been used yet in {@link #createFor(TestCoverageBuilder)}. */ - public List createTestInfosWithoutCoverage() { - ArrayList results = new ArrayList<>(); - for (TestDetails testDetails : testDetailsMap.values()) { - if (!processedTestUniformPaths.contains(testDetails.uniformPath)) { - TestInfoBuilder testInfo = new TestInfoBuilder(testDetails.uniformPath); - testInfo.setDetails(testDetails); - testInfo.setExecution(testExecutionsMap.get(testDetails.uniformPath)); - results.add(testInfo.build()); - processedTestUniformPaths.add(testDetails.uniformPath); - } - } - for (TestExecution testExecution : testExecutionsMap.values()) { - if (!processedTestUniformPaths.contains(testExecution.getUniformPath())) { - System.err.println("Test " + testExecution.getUniformPath() + " was executed but no coverage was found. " + - "Please make sure that you did provide all relevant exec files and that the test IDs passed to " + - "the agent match the ones from the provided test execution list."); - processedTestUniformPaths.add(testExecution.getUniformPath()); - } - } - return results; - } - - /** - * Strips parameterized test arguments when the full path given in the coverage file cannot be found in the test - * details. - */ - private String resolveUniformPath(String originalUniformPath) { - String uniformPath = originalUniformPath; - TestDetails testDetails = testDetailsMap.get(uniformPath); - if (testDetails == null) { - uniformPath = TestwiseCoverageReportBuilder - .stripParameterizedTestArguments(uniformPath); - } - return uniformPath; - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/util/AntPatternIncludeFilter.java b/report-generator/src/main/java/com/teamscale/report/util/AntPatternIncludeFilter.java deleted file mode 100644 index 309e22750..000000000 --- a/report-generator/src/main/java/com/teamscale/report/util/AntPatternIncludeFilter.java +++ /dev/null @@ -1,57 +0,0 @@ -/*-------------------------------------------------------------------------+ -| | -| Copyright (c) 2009-2018 CQSE GmbH | -| | -+-------------------------------------------------------------------------*/ -package com.teamscale.report.util; - -import com.teamscale.client.AntPatternUtils; -import com.teamscale.client.FileSystemUtils; - -import java.util.List; -import java.util.function.Predicate; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -/** - * Applies ANT include and exclude patterns to paths. - */ -public class AntPatternIncludeFilter implements Predicate { - - /** The include filters. Empty means include everything. */ - private final List locationIncludeFilters; - - /** The exclude filters. Empty means exclude nothing. */ - private final List locationExcludeFilters; - - /** Constructor. */ - public AntPatternIncludeFilter(List locationIncludeFilters, List locationExcludeFilters) { - this.locationIncludeFilters = locationIncludeFilters.stream().map( - filter -> AntPatternUtils.convertPattern(filter, false)).collect(Collectors.toList()); - this.locationExcludeFilters = locationExcludeFilters.stream().map( - filter -> AntPatternUtils.convertPattern(filter, false)).collect(Collectors.toList()); - } - - /** {@inheritDoc} */ - @Override - public boolean test(String path) { - return !isFiltered(FileSystemUtils.normalizeSeparators(path)); - } - - /** - * Returns true if the given class file location (normalized to forward slashes as path separators) - * should not be analyzed. - *

- * Exclude filters overrule include filters. - */ - private boolean isFiltered(String location) { - // first check includes - if (!locationIncludeFilters.isEmpty() - && locationIncludeFilters.stream().noneMatch(filter -> filter.matcher(location).matches())) { - return true; - } - // only if they match, check excludes - return locationExcludeFilters.stream().anyMatch(filter -> filter.matcher(location).matches()); - } - -} diff --git a/report-generator/src/main/java/com/teamscale/report/util/BashFileSkippingInputStream.java b/report-generator/src/main/java/com/teamscale/report/util/BashFileSkippingInputStream.java deleted file mode 100644 index 59c9102d4..000000000 --- a/report-generator/src/main/java/com/teamscale/report/util/BashFileSkippingInputStream.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.teamscale.report.util; - -import java.io.BufferedInputStream; -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; - -/** - * Handles executable spring-boot jar files that prepend a bash file to the beginning of the ZIP file to make it - * directly executable without "java -jar my.jar". We just skip the bash file until we find the zip file header. - */ -public class BashFileSkippingInputStream extends FilterInputStream { - - /** - * Wraps the given input stream in a BufferedInputStream and consumes all bytes until a zip file header is found. - */ - public BashFileSkippingInputStream(InputStream in) throws IOException { - super(new BufferedInputStream(in)); - consumeUntilZipHeader(); - } - - /** - * Reads the stream until the zip file header "50 4B 03 04" is found or EOF is reached. After calling the method the - * read pointer points to the first byte of the zip file header. - */ - private void consumeUntilZipHeader() throws IOException { - byte[] buffer = new byte[8192]; - in.mark(buffer.length); - int count = in.read(buffer, 0, buffer.length); - while (count > 0) { - for (int i = 0; i < count - 3; i++) { - if (buffer[i] == 0x50 && buffer[i + 1] == 0x4B && buffer[i + 2] == 0x03 && buffer[i + 3] == 0x04) { - in.reset(); - in.skip(i); - return; - } - } - - // Reset mark to 3 bytes before the end of the previously read buffer end to - // also detect a zip header when it spans over two buffers - in.reset(); - in.skip(buffer.length - 3); - in.mark(buffer.length); - count = in.read(buffer, 0, buffer.length); - } - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/util/ClasspathWildcardIncludeFilter.java b/report-generator/src/main/java/com/teamscale/report/util/ClasspathWildcardIncludeFilter.java deleted file mode 100644 index 56d9480c5..000000000 --- a/report-generator/src/main/java/com/teamscale/report/util/ClasspathWildcardIncludeFilter.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.teamscale.report.util; - -import com.teamscale.client.FileSystemUtils; -import com.teamscale.client.StringUtils; -import org.jacoco.core.runtime.WildcardMatcher; -import org.jacoco.report.JavaNames; - -/*** - * Tests given class file paths against call name patterns. - * E.g. "/some/file/path/test.jar@my/package/Test.class" matches "my/package/*" or "my/package/Test" - */ -public class ClasspathWildcardIncludeFilter { - - /** - * Include patterns to apply during JaCoCo's traversal of class files. If null then everything is included. - */ - private WildcardMatcher locationIncludeFilters = null; - - /** - * Exclude patterns to apply during JaCoCo's traversal of class files. If null then nothing is excluded. - */ - private WildcardMatcher locationExcludeFilters = null; - - /** - * Constructor. - * - * @param locationIncludeFilters Colon separated list of wildcard include patterns for fully qualified class names - * or null for no includes. See {@link WildcardMatcher} for the pattern syntax. - * @param locationExcludeFilters Colon separated list of wildcard exclude patterns for fully qualified class names - * or null for no excludes.See {@link WildcardMatcher} for the pattern syntax. - */ - public ClasspathWildcardIncludeFilter(String locationIncludeFilters, String locationExcludeFilters) { - if (locationIncludeFilters != null && !locationIncludeFilters.isEmpty()) { - this.locationIncludeFilters = new WildcardMatcher(locationIncludeFilters); - } - if (locationExcludeFilters != null && !locationExcludeFilters.isEmpty()) { - this.locationExcludeFilters = new WildcardMatcher(locationExcludeFilters); - } - } - - /** - * Tests if the given file path (e.g. "/some/file/path/test.jar@my/package/Test.class" or "org/mypackage/MyClass" - */ - public boolean isIncluded(String path) { - String className = getClassName(path); - // first check includes - if (locationIncludeFilters != null && !locationIncludeFilters.matches(className)) { - return false; - } - // if they match, check excludes - return locationExcludeFilters == null || !locationExcludeFilters.matches(className); - } - - /** - * Returns the normalized class name of the given class file's path. I.e. turns something like - * "/opt/deploy/some.jar@com/teamscale/Class.class" into something like "com.teamscale.Class". - */ - /* package */ - static String getClassName(String path) { - String[] parts = FileSystemUtils.normalizeSeparators(path).split("@"); - if (parts.length == 0) { - return ""; - } - - String pathInsideJar = parts[parts.length - 1]; - if (path.toLowerCase().endsWith(".class")) { - pathInsideJar = StringUtils.removeLastPart(pathInsideJar, '.'); - } - return new JavaNames().getQualifiedClassName(pathInsideJar); - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/util/CommandLineLogger.java b/report-generator/src/main/java/com/teamscale/report/util/CommandLineLogger.java deleted file mode 100644 index fe112bf8b..000000000 --- a/report-generator/src/main/java/com/teamscale/report/util/CommandLineLogger.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.teamscale.report.util; - -/** Logger that prints all output to the console. */ -public class CommandLineLogger implements ILogger { - - @Override - public void debug(String message) { - System.out.println(message); - } - - @Override - public void info(String message) { - System.out.println(message); - } - - @Override - public void warn(String message) { - System.err.println(message); - } - - @Override - public void warn(String message, Throwable throwable) { - System.err.println(message); - if (throwable != null) { - throwable.printStackTrace(); - } - } - - @Override - public void error(Throwable throwable) { - throwable.printStackTrace(); - } - - @Override - public void error(String message, Throwable throwable) { - System.err.println(message); - if (throwable != null) { - throwable.printStackTrace(); - } - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/util/ILogger.java b/report-generator/src/main/java/com/teamscale/report/util/ILogger.java deleted file mode 100644 index b4d23c3c3..000000000 --- a/report-generator/src/main/java/com/teamscale/report/util/ILogger.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.teamscale.report.util; - -/** - * Minimal logging interface. - *

- * We use this to work around some strange problems when using log4j from the Teamscale Gradle plugin. - */ -public interface ILogger { - - /** Logs at debug level. */ - void debug(String message); - - /** Logs at info level. */ - void info(String message); - - /** Logs at warning level. */ - void warn(String message); - - /** Logs at warning level. The given {@link Throwable} may be null. */ - void warn(String message, Throwable throwable); - - /** Logs at error level. */ - void error(Throwable throwable); - - /** Logs at error level. The given {@link Throwable} may be null. */ - void error(String message, Throwable throwable); - - /** Logs at error level. */ - default void error(String message) { - error(message, null); - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/util/SortedIntList.java b/report-generator/src/main/java/com/teamscale/report/util/SortedIntList.java deleted file mode 100644 index afa4a7ef8..000000000 --- a/report-generator/src/main/java/com/teamscale/report/util/SortedIntList.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.teamscale.report.util; - -/** - * Performant implementation of a deduplicated sorted integer list that assumes that insertions mainly happen at the end - * and that input is already sorted. - */ -public class SortedIntList { - - /** - * The list of values in sorted order and without duplicates. The list might be bigger than the number of elements. - */ - protected int[] list; - - /** The number of actual elements in the list. */ - private int count; - - public SortedIntList() { - list = new int[64]; - } - - /** Adds the given value to the list at the correct location, ignoring duplicates. */ - public boolean add(int value) { - int high = count; - int low = 0; - - if (isEmpty()) { - list[0] = value; - count = 1; - return true; - } - - // Perform binary search to find target location - do { - int p = (low + high) >>> 1; - if (value < list[p]) { - high = p; - } else if (value == list[p]) { - // Element already exists in the list - return false; - } else { - low = p + 1; - } - } while (low < high); - - if (count == list.length) { - int[] n = new int[list.length * 2]; - System.arraycopy(list, 0, n, 0, count); - list = n; - } - - if (low < count) { - System.arraycopy(list, low, list, low + 1, count - low); - } - list[low] = value; - count++; - return true; - } - - /** Inserts all values from the given list, ignoring duplicates. */ - public void addAll(SortedIntList input) { - for (int i = 0; i < input.size(); i++) { - add(input.get(i)); - } - } - - /** Returns the size of the list. */ - public int size() { - return count; - } - - /** Returns whether the list is empty. */ - public boolean isEmpty() { - return count == 0; - } - - /** Returns the i-th element of the list. */ - public int get(int i) { - return list[i]; - } -} \ No newline at end of file diff --git a/report-generator/src/main/java/org/jacoco/core/internal/analysis/CachingClassAnalyzer.java b/report-generator/src/main/java/org/jacoco/core/internal/analysis/CachingClassAnalyzer.java deleted file mode 100644 index 406b0877b..000000000 --- a/report-generator/src/main/java/org/jacoco/core/internal/analysis/CachingClassAnalyzer.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.jacoco.core.internal.analysis; - -import com.teamscale.report.testwise.jacoco.cache.ClassCoverageLookup; -import org.jacoco.core.internal.flow.MethodProbesVisitor; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.tree.MethodNode; - -/** - * Analyzes a class to reconstruct probe information. - *

- * A probe lookup holds for a single class which probe belongs to which lines. The actual filling of the - * {@link ClassCoverageLookup} happens in {@link CachingInstructionsBuilder}. - */ -public class CachingClassAnalyzer extends ClassAnalyzer { - - /** The cache, which contains a probe lookups for the current class. */ - private final ClassCoverageLookup classCoverageLookup; - - /** - * Creates a new analyzer that builds coverage data for a class. - * - * @param classCoverageLookup cache for the class' probes - * @param coverage coverage node for the analyzed class data - * @param stringPool shared pool to minimize the number of {@link String} instances - */ - public CachingClassAnalyzer(ClassCoverageLookup classCoverageLookup, ClassCoverageImpl coverage, StringPool stringPool) { - super(coverage, null, stringPool); - this.classCoverageLookup = classCoverageLookup; - } - - @Override - public void visitSource(String source, String debug) { - super.visitSource(source, debug); - classCoverageLookup.setSourceFileName(source); - } - - @Override - public MethodProbesVisitor visitMethod(final int access, final String name, - final String desc, final String signature, final String[] exceptions) { - final CachingInstructionsBuilder builder = new CachingInstructionsBuilder(classCoverageLookup); - - return new MethodAnalyzer(builder) { - - @Override - public void accept(final MethodNode methodNode, - final MethodVisitor methodVisitor) { - super.accept(methodNode, methodVisitor); - builder.fillCache(); - } - }; - } - - @Override - public void visitTotalProbeCount(final int count) { - classCoverageLookup.setTotalProbeCount(count); - } -} diff --git a/report-generator/src/main/java/org/jacoco/core/internal/analysis/CachingInstructionsBuilder.java b/report-generator/src/main/java/org/jacoco/core/internal/analysis/CachingInstructionsBuilder.java index 4701cbadd..f91e52202 100644 --- a/report-generator/src/main/java/org/jacoco/core/internal/analysis/CachingInstructionsBuilder.java +++ b/report-generator/src/main/java/org/jacoco/core/internal/analysis/CachingInstructionsBuilder.java @@ -1,7 +1,7 @@ package org.jacoco.core.internal.analysis; import com.teamscale.report.testwise.jacoco.cache.ClassCoverageLookup; -import com.teamscale.report.util.SortedIntList; +import com.teamscale.report.util.CompactLines; import org.jacoco.core.analysis.ISourceNode; import org.jacoco.core.internal.flow.LabelInfo; import org.objectweb.asm.Label; @@ -170,7 +170,7 @@ public void fillCache() { // We need this because JaCoCo does not insert a probe after every line. for (CoveredProbe coveredProbe : coveredProbes) { Instruction instruction = coveredProbe.instruction; - SortedIntList coveredLines = new SortedIntList(); + CompactLines coveredLines = new CompactLines(); while (instruction != null) { if (instruction.getLine() != -1) { // Only add the line number if one is associated with the instruction. @@ -231,4 +231,4 @@ void wire() { } -} +} \ No newline at end of file diff --git a/report-generator/src/main/kotlin/com/teamscale/report/EDuplicateClassFileBehavior.kt b/report-generator/src/main/kotlin/com/teamscale/report/EDuplicateClassFileBehavior.kt new file mode 100644 index 000000000..9b21854b0 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/EDuplicateClassFileBehavior.kt @@ -0,0 +1,15 @@ +package com.teamscale.report + +/** + * Behavior when two non-identical class files with the same package name are found. + */ +enum class EDuplicateClassFileBehavior { + /** Completely ignores it. */ + IGNORE, + + /** Prints a warning to the logger. */ + WARN, + + /** Fails and stops further processing. */ + FAIL +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/ReportUtils.kt b/report-generator/src/main/kotlin/com/teamscale/report/ReportUtils.kt new file mode 100644 index 000000000..7e1ebc962 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/ReportUtils.kt @@ -0,0 +1,88 @@ +package com.teamscale.report + +import com.fasterxml.jackson.core.JsonProcessingException +import com.teamscale.client.FileSystemUtils +import com.teamscale.client.JsonUtils +import com.teamscale.client.TestDetails +import com.teamscale.report.testwise.ETestArtifactFormat +import com.teamscale.report.testwise.model.TestExecution +import com.teamscale.report.testwise.model.TestwiseCoverageReport +import java.io.File +import java.io.FileFilter +import java.io.IOException +import java.util.* + +/** Utilities for generating reports. */ +object ReportUtils { + /** Converts to given test list to a json report and writes it to the given file. */ + @Throws(IOException::class) + @JvmStatic + fun writeTestListReport(reportFile: File, report: List) { + writeReportToFile(reportFile, report) + } + + /** Converts to given test execution report to a json report and writes it to the given file. */ + @Throws(IOException::class) + @JvmStatic + fun writeTestExecutionReport(reportFile: File, report: List) { + writeReportToFile(reportFile, report) + } + + /** Converts to given testwise coverage report to a json report and writes it to the given file. */ + @Throws(IOException::class) + fun writeTestwiseCoverageReport(reportFile: File, report: TestwiseCoverageReport) { + writeReportToFile(reportFile, report) + } + + /** Converts to given report to a json string. For testing only. */ + @JvmStatic + @Throws(JsonProcessingException::class) + fun getTestwiseCoverageReportAsString( + report: TestwiseCoverageReport + ): String = JsonUtils.serialize(report) + + /** Writes the report object to the given file as json. */ + @Throws(IOException::class) + private fun writeReportToFile(reportFile: File, report: T) { + val directory = reportFile.getParentFile() + if (!directory.isDirectory() && !directory.mkdirs()) { + throw IOException("Failed to create directory " + directory.absolutePath) + } + JsonUtils.serializeToFile(reportFile, report) + } + + /** Recursively lists all files in the given directory that match the specified extension. */ + @Throws(IOException::class) + @JvmStatic + fun readObjects( + format: ETestArtifactFormat, + clazz: Class>, + directoriesOrFiles: List + ) = listFiles(format, directoriesOrFiles) + .mapNotNull { JsonUtils.deserializeFile(it, clazz) } + .flatMap { listOf(*it) } + + /** Recursively lists all files of the given artifact type. */ + @JvmStatic + fun listFiles( + format: ETestArtifactFormat, + directoriesOrFiles: List + ) = directoriesOrFiles.flatMap { directoryOrFile -> + when { + directoryOrFile.isDirectory() -> { + FileSystemUtils.listFilesRecursively(directoryOrFile) { + it.isOfArtifactFormat(format) + } + } + directoryOrFile.isOfArtifactFormat(format) -> { + listOf(directoryOrFile) + } + else -> emptyList() + } + } + + private fun File.isOfArtifactFormat(format: ETestArtifactFormat) = + isFile() && + getName().startsWith(format.filePrefix) && + FileSystemUtils.getFileExtension(this).equals(format.extension, ignoreCase = true) +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/jacoco/CoverageFile.kt b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/CoverageFile.kt new file mode 100644 index 000000000..1e8754175 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/CoverageFile.kt @@ -0,0 +1,95 @@ +package com.teamscale.report.jacoco + +import okhttp3.MultipartBody +import okhttp3.RequestBody +import java.io.* +import java.util.* + +/** + * Represents a coverage file on disk. The main purpose is to avoid reading the + * entire file into memory as this dramatically increases the memory footprint + * of the JVM which might run out of memory because of this. + * + * The object internally holds a counter of how many references to the file are + * currently held. This allows to share the same file for multiple uploads and + * deleting it once all uploads have succeeded. Use [.acquireReference] + * to make the object aware that it was passed to another uploader and + * [.delete] to signal that you no longer intend to access the file. + */ +data class CoverageFile(private val coverageFile: File) { + private var referenceCounter = 0 + + /** + * Marks the file as being used by an additional uploader. This ensures that the + * file is not deleted until all users have signed via [.delete] that + * they no longer intend to access the file. + */ + fun acquireReference(): CoverageFile { + referenceCounter++ + return this + } + + /** + * Copies the coverage File in blocks from the disk to the output stream to + * avoid having to read the entire file into memory. + */ + @Throws(IOException::class) + fun copyStream(outputStream: OutputStream) { + coverageFile.inputStream().use { input -> + input.copyTo(outputStream) + } + } + + /** + * Get the filename of the coverage file on disk without its extension + */ + val nameWithoutExtension: String + get() = coverageFile.nameWithoutExtension + + /** Get the filename of the coverage file. */ + val name: String + get() = coverageFile.name + + /** + * Delete the coverage file from disk + */ + @Throws(IOException::class) + fun delete() { + referenceCounter-- + if (referenceCounter <= 0) { + coverageFile.delete() + } + } + + /** + * Create a [okhttp3.MultipartBody] form body with the contents of the + * coverage file. + */ + fun createFormRequestBody(): RequestBody = + RequestBody.create(MultipartBody.FORM, coverageFile) + + /** + * Get the [java.io.OutputStream] in order to write to the coverage file. + * + * @throws IOException + * If the file did not exist yet and could not be created + */ + @get:Throws(IOException::class) + val outputStream: OutputStream + get() { + return runCatching { + coverageFile.outputStream() + }.getOrElse { + throw IOException( + ("Could not create temporary coverage file" + this + ". " + + "This is used to cache the coverage file on disk before uploading it to its final destination. " + + "This coverage is lost. Please fix the underlying issue to avoid losing coverage."), it + ) + } + } + + /** + * {@inheritDoc} + */ + override fun toString(): String = coverageFile.absolutePath +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/jacoco/EmptyReportException.kt b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/EmptyReportException.kt new file mode 100644 index 000000000..efcf4ba15 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/EmptyReportException.kt @@ -0,0 +1,6 @@ +package com.teamscale.report.jacoco + +/** + * Exception indicating that the generated report was empty and no [CoverageFile] was written to disk. + */ +class EmptyReportException(message: String) : Exception(message) diff --git a/report-generator/src/main/kotlin/com/teamscale/report/jacoco/FilteringAnalyzer.kt b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/FilteringAnalyzer.kt new file mode 100644 index 000000000..7a12e7dbe --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/FilteringAnalyzer.kt @@ -0,0 +1,88 @@ +/*-------------------------------------------------------------------------+ +| | +| Copyright (c) 2009-2018 CQSE GmbH | +| | ++-------------------------------------------------------------------------*/ +package com.teamscale.report.jacoco + +import com.teamscale.report.util.BashFileSkippingInputStream +import com.teamscale.report.util.ClasspathWildcardIncludeFilter +import com.teamscale.report.util.ILogger +import org.jacoco.core.analysis.ICoverageVisitor +import org.jacoco.core.data.ExecutionDataStore +import java.io.IOException +import java.io.InputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream + +/** + * [org.jacoco.core.analysis.Analyzer] that filters the analyzed class files based on a given predicate. + * + * @param executionData The execution data store. + * @param coverageVisitor The coverage visitor. + * @param locationIncludeFilter The filter for the analyzed class files. + * @param logger The logger. + */ +open class FilteringAnalyzer( + executionData: ExecutionDataStore?, + coverageVisitor: ICoverageVisitor?, + private val locationIncludeFilter: ClasspathWildcardIncludeFilter, + private val logger: ILogger +) : OpenAnalyzer(executionData, coverageVisitor) { + /** {@inheritDoc} */ + @Throws(IOException::class) + override fun analyzeAll(input: InputStream, location: String): Int { + if (location.endsWith(".class") && !locationIncludeFilter.isIncluded(location)) { + logger.debug("Excluding class file $location") + return 1 + } + if (location.endsWith(".jar")) { + return analyzeJar(input, location) + } + return super.analyzeAll(input, location) + } + + @Throws(IOException::class) + override fun analyzeClass(buffer: ByteArray, location: String) { + try { + analyzeClass(buffer) + } catch (cause: RuntimeException) { + if (cause.isUnsupportedClassFile()) { + logger.error(cause.message + " in " + location) + } else { + throw analyzerError(location, cause) + } + } + } + + /** + * Checks if the error indicates that the class file might be newer than what is currently supported by + * JaCoCo. The concrete error message seems to depend on the used JVM, so we only check for "Unsupported" which seems + * to be common amongst all of them. + */ + private fun RuntimeException.isUnsupportedClassFile() = + this is IllegalArgumentException && message?.startsWith("Unsupported") == true + + /** + * Copied from [org.jacoco.core.analysis.Analyzer.analyzeZip] renamed to analyzeJar + * and added wrapping [BashFileSkippingInputStream]. + */ + @Throws(IOException::class) + protected open fun analyzeJar(input: InputStream, location: String): Int { + ZipInputStream(BashFileSkippingInputStream(input)).use { zip -> + return generateSequence { zip.nextEntry(location) } + .map { entry -> analyzeAll(zip, "$location@${entry.name}") } + .sum() + } + } + + /** Copied from [org.jacoco.core.analysis.Analyzer.nextEntry]. */ + @Throws(IOException::class) + private fun ZipInputStream.nextEntry(location: String): ZipEntry? { + try { + return nextEntry + } catch (e: IOException) { + throw analyzerError(location, e) + } + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/jacoco/JaCoCoXmlReportGenerator.kt b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/JaCoCoXmlReportGenerator.kt new file mode 100644 index 000000000..b8da73938 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/JaCoCoXmlReportGenerator.kt @@ -0,0 +1,107 @@ +package com.teamscale.report.jacoco + +import com.teamscale.report.EDuplicateClassFileBehavior +import com.teamscale.report.jacoco.dump.Dump +import com.teamscale.report.util.ClasspathWildcardIncludeFilter +import com.teamscale.report.util.ILogger +import org.jacoco.core.analysis.IBundleCoverage +import org.jacoco.core.data.ExecutionDataStore +import org.jacoco.core.data.SessionInfo +import org.jacoco.report.xml.XMLFormatter +import java.io.File +import java.io.IOException +import java.io.OutputStream + +/** + * Creates an XML report from binary execution data. + * + * @param codeDirectoriesOrArchives Directories and zip files that contain class files. + * @param locationIncludeFilter Include filter to apply to all locations during class file traversal. + * @param duplicateClassFileBehavior Whether to ignore non-identical duplicates of class files. + * @param ignoreUncoveredClasses Whether to remove uncovered classes from the report. + * @param logger The logger. + */ +class JaCoCoXmlReportGenerator( + private val codeDirectoriesOrArchives: List, + private val locationIncludeFilter: ClasspathWildcardIncludeFilter, + private val duplicateClassFileBehavior: EDuplicateClassFileBehavior, + private val ignoreUncoveredClasses: Boolean, + private val logger: ILogger +) { + /** + * Creates the report and writes it to a file. + * + * @return The file object of for the converted report or null if it could not be created + */ + @Throws(IOException::class, EmptyReportException::class) + fun convert(dump: Dump, filePath: File): CoverageFile { + val coverageFile = CoverageFile(filePath) + convertToReport(coverageFile, dump) + return coverageFile + } + + /** Creates the report. */ + @Throws(IOException::class, EmptyReportException::class) + private fun convertToReport(coverageFile: CoverageFile, dump: Dump) { + val mergedStore = dump.store + analyzeStructureAndAnnotateCoverage(mergedStore).apply { + checkForEmptyReport() + coverageFile.outputStream.use { outputStream -> + createReport( + outputStream, this, dump.info, mergedStore) + } + } + } + + @Throws(EmptyReportException::class) + private fun IBundleCoverage.checkForEmptyReport() { + if (packages.isEmpty() || lineCounter.totalCount == 0) { + throw EmptyReportException("The generated coverage report is empty. $MOST_LIKELY_CAUSE_MESSAGE") + } + if (lineCounter.coveredCount == 0) { + throw EmptyReportException("The generated coverage report does not contain any covered source code lines. $MOST_LIKELY_CAUSE_MESSAGE") + } + } + + /** + * Analyzes the structure of the class files in [.codeDirectoriesOrArchives] and builds an in-memory coverage + * report with the coverage in the given store. + */ + @Throws(IOException::class) + private fun analyzeStructureAndAnnotateCoverage(store: ExecutionDataStore): IBundleCoverage { + val coverageBuilder = TeamscaleCoverageBuilder( + logger, duplicateClassFileBehavior, ignoreUncoveredClasses + ) + + codeDirectoriesOrArchives.forEach { file -> + FilteringAnalyzer(store, coverageBuilder, locationIncludeFilter, logger) + .analyzeAll(file) + } + + return coverageBuilder.getBundle("dummybundle") + } + + companion object { + /** Part of the error message logged when validating the coverage report fails. */ + private const val MOST_LIKELY_CAUSE_MESSAGE = "Most likely you did not configure the agent correctly." + + " Please check that the includes and excludes options are set correctly so the relevant code is included." + + " If in doubt, first include more code and then iteratively narrow the patterns down to just the relevant code." + + " If you have specified the class-dir option, please make sure it points to a directory containing the" + + " class files/jars/wars/ears/etc. for which you are trying to measure code coverage." + + /** Creates an XML report based on the given session and coverage data. */ + @Throws(IOException::class) + private fun createReport( + output: OutputStream, + bundleCoverage: IBundleCoverage, + sessionInfo: SessionInfo?, + store: ExecutionDataStore + ) { + XMLFormatter().createVisitor(output).apply { + visitInfo(listOf(sessionInfo), store.contents) + visitBundle(bundleCoverage, null) + visitEnd() + } + } + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/jacoco/TeamscaleCoverageBuilder.kt b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/TeamscaleCoverageBuilder.kt new file mode 100644 index 000000000..d049df8dd --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/TeamscaleCoverageBuilder.kt @@ -0,0 +1,62 @@ +/*-------------------------------------------------------------------------+ +| | +| Copyright (c) 2009-2018 CQSE GmbH | +| | ++-------------------------------------------------------------------------*/ +package com.teamscale.report.jacoco + +import com.teamscale.report.EDuplicateClassFileBehavior +import com.teamscale.report.util.ILogger +import org.jacoco.core.analysis.CoverageBuilder +import org.jacoco.core.analysis.IClassCoverage +import org.jacoco.core.analysis.ICounter +import org.jacoco.core.internal.analysis.BundleCoverageImpl + +/** + * Modified [CoverageBuilder] can ignore non-identical duplicate classes or classes without coverage. In addition, + * coverage returned via [.getBundle] will only return source file coverage because Teamscale does not + * need class coverage anyway. This reduces XML size by approximately half. + * + * @param logger The logger. + * @param duplicateClassFileBehavior How to behave if duplicate class files are encountered. + * @param ignoreUncoveredClasses Whether to ignore uncovered classes (i.e. leave them out of the report). + */ +internal class TeamscaleCoverageBuilder( + private val logger: ILogger, + private val duplicateClassFileBehavior: EDuplicateClassFileBehavior, + private val ignoreUncoveredClasses: Boolean +) : CoverageBuilder() { + /** Just returns source file coverage, because Teamscale does not need class coverage. */ + override fun getBundle(name: String) = + BundleCoverageImpl(name, emptyList(), sourceFiles) + + /** {@inheritDoc} */ + override fun visitCoverage(coverage: IClassCoverage) { + if (ignoreUncoveredClasses && (coverage.classCounter.status and ICounter.FULLY_COVERED) == 0) { + return + } + + try { + super.visitCoverage(coverage) + } catch (e: IllegalStateException) { + when (duplicateClassFileBehavior) { + EDuplicateClassFileBehavior.IGNORE -> return + EDuplicateClassFileBehavior.WARN -> { + // we do not log the exception here as it does not provide additional valuable information + // and may confuse users into thinking there's a serious + // problem with the agent due to the stack traces in the log + logger.warn("Ignoring duplicate, non-identical class file for class ${coverage.name} compiled " + + "from source file ${coverage.sourceFileName}. This happens when a class with the same " + + "fully-qualified name is loaded twice but the two loaded class files are not identical. " + + "A common reason for this is that the same library or shared code is included twice in " + + "your application but in two different versions. The produced coverage for this class " + + "may not be accurate or may even be unusable. To fix this problem, please resolve the " + + "conflict between both class files in your application.") + return + } + + else -> throw e + } + } + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/jacoco/dump/Dump.kt b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/dump/Dump.kt new file mode 100644 index 000000000..b2f56f848 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/dump/Dump.kt @@ -0,0 +1,15 @@ +/*-------------------------------------------------------------------------+ +| | +| Copyright (c) 2009-2018 CQSE GmbH | +| | ++-------------------------------------------------------------------------*/ +package com.teamscale.report.jacoco.dump + +import org.jacoco.core.data.ExecutionDataStore +import org.jacoco.core.data.SessionInfo + +/** All data received in one dump. */ +data class Dump( + val info: SessionInfo, + val store: ExecutionDataStore +) diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/ETestArtifactFormat.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/ETestArtifactFormat.kt new file mode 100644 index 000000000..3e9b15c73 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/ETestArtifactFormat.kt @@ -0,0 +1,23 @@ +package com.teamscale.report.testwise + +/** Enum of test artifacts that can be converted to a full testwise coverage report later on. */ +enum class ETestArtifactFormat( + /** A readable name for the report type. */ + val readableName: String, + /** Prefix to use when writing the report to the file system. */ + val filePrefix: String, + /** File extension of the report. */ + val extension: String +) { + /** A json list of tests ([com.teamscale.client.TestDetails]). */ + TEST_LIST("Test List", "test-list", "json"), + + /** A json list of test executions ([com.teamscale.report.testwise.model.TestExecution]). */ + TEST_EXECUTION("Test Execution", "test-execution", "json"), + + /** Binary jacoco test coverage (.exec file). */ + JACOCO("Jacoco", "", "exec"), + + /** Google closure coverage files with additional uniformPath entries. */ + CLOSURE("Closure Coverage", "closure-coverage", "json") +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/TestwiseCoverageReportWriter.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/TestwiseCoverageReportWriter.kt new file mode 100644 index 000000000..b7eb6a329 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/TestwiseCoverageReportWriter.kt @@ -0,0 +1,96 @@ +package com.teamscale.report.testwise + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter +import com.teamscale.client.JsonUtils +import com.teamscale.client.StringUtils +import com.teamscale.report.testwise.model.TestInfo +import com.teamscale.report.testwise.model.builder.TestCoverageBuilder +import com.teamscale.report.testwise.model.factory.TestInfoFactory +import java.io.File +import java.io.IOException +import java.io.OutputStream +import java.nio.file.Files +import java.util.function.Consumer + +/** + * Writes out a [com.teamscale.report.testwise.model.TestwiseCoverageReport] one [TestInfo] after the other + * so that we do not need to keep them all in memory during the conversion. + */ +class TestwiseCoverageReportWriter( + /** Factory for converting [TestCoverageBuilder] objects to [TestInfo]s. */ + private val testInfoFactory: TestInfoFactory, private val outputFile: File, + /** After how many written tests a new file should be started. */ + private val splitAfter: Int +) : Consumer, + AutoCloseable { + /** Writer instance to where the [com.teamscale.report.testwise.model.TestwiseCoverageReport] is written to. */ + private var jsonGenerator: JsonGenerator? = null + + /** Number of tests written to the file. */ + private var testsWritten: Int = 0 + + /** Number of test files that have been written. */ + private var testFileCounter: Int = 0 + + init { + startReport() + } + + override fun accept(testCoverageBuilder: TestCoverageBuilder) { + val testInfo = testInfoFactory.createFor(testCoverageBuilder) + try { + writeTestInfo(testInfo) + } catch (e: IOException) { + // Need to be wrapped in RuntimeException as Consumer does not allow to throw a checked Exception + throw RuntimeException("Writing test info to report failed.", e) + } + } + + @Throws(IOException::class) + override fun close() { + testInfoFactory.createTestInfosWithoutCoverage().forEach { testInfo -> + writeTestInfo(testInfo) + } + endReport() + } + + @Throws(IOException::class) + private fun startReport() { + testFileCounter++ + val outputStream = Files.newOutputStream(getOutputFile(testFileCounter).toPath()) + jsonGenerator = JsonUtils.createFactory().createGenerator(outputStream).apply { + prettyPrinter = DefaultPrettyPrinter() + writeStartObject() + writeFieldName("tests") + writeStartArray() + } + } + + private fun getOutputFile(testFileCounter: Int): File { + var name = outputFile.nameWithoutExtension + + name = "$name-$testFileCounter.json" + return File(outputFile.getParent(), name) + } + + @Throws(IOException::class) + private fun writeTestInfo(testInfo: TestInfo?) { + if (testsWritten >= splitAfter) { + endReport() + testsWritten = 0 + startReport() + } + jsonGenerator?.writeObject(testInfo) + testsWritten++ + } + + @Throws(IOException::class) + private fun endReport() { + jsonGenerator?.let { + it.writeEndArray() + it.writeEndObject() + it.close() + } + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/CachingExecutionDataReader.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/CachingExecutionDataReader.kt new file mode 100644 index 000000000..735ef1b1e --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/CachingExecutionDataReader.kt @@ -0,0 +1,113 @@ +package com.teamscale.report.testwise.jacoco + +import com.teamscale.report.EDuplicateClassFileBehavior +import com.teamscale.report.jacoco.dump.Dump +import com.teamscale.report.testwise.jacoco.cache.AnalyzerCache +import com.teamscale.report.testwise.jacoco.cache.ProbesCache +import com.teamscale.report.testwise.model.builder.TestCoverageBuilder +import com.teamscale.report.util.ClasspathWildcardIncludeFilter +import com.teamscale.report.util.ILogger +import org.jacoco.core.data.ExecutionDataStore +import java.io.File +import java.util.function.Consumer + +/** + * Helper class for analyzing class files, reading execution data, and converting them to coverage data. + */ +open class CachingExecutionDataReader( + private val logger: ILogger, + private val classesDirectories: Collection, + private val locationIncludeFilter: ClasspathWildcardIncludeFilter, + private val duplicateClassFileBehavior: EDuplicateClassFileBehavior +) { + private val probeCache: ProbesCache by lazy { + ProbesCache(logger, duplicateClassFileBehavior) + } + + /** + * Analyzes class directories and creates a lookup of probes to methods. + */ + fun analyzeClassDirs() { + if (classesDirectories.isEmpty()) { + logger.warn("No class directories found for caching.") + return + } + val analyzer = AnalyzerCache(probeCache, locationIncludeFilter, logger) + val classCount = classesDirectories + .filter { it.exists() } + .sumOf { analyzeDirectory(it, analyzer) } + + validateAnalysisResult(classCount) + } + + /** + * Analyzes the specified directory, logging errors if any occur. + */ + private fun analyzeDirectory(classDir: File, analyzer: AnalyzerCache) = + runCatching { analyzer.analyzeAll(classDir) } + .onFailure { e -> logger.error("Failed to analyze class files in $classDir! " + + "Maybe the folder contains incompatible class files. Coverage for class files " + + "in this folder will be ignored.", e) } + .getOrDefault(0) + + /** + * Builds a consumer for coverage data. + */ + fun buildCoverageConsumer( + locationIncludeFilter: ClasspathWildcardIncludeFilter, + nextConsumer: Consumer + ) = DumpConsumer(logger, locationIncludeFilter, nextConsumer) + + /** + * Logs errors if no classes were analyzed or if the filter excluded all files. + */ + private fun validateAnalysisResult(classCount: Int) { + val directoryList = classesDirectories.joinToString(",") { it.path } + when { + classCount == 0 -> logger.error("No class files found in directories: $directoryList") + probeCache.isEmpty -> logger.error( + "None of the $classCount class files found in the given directories match the configured include/exclude patterns! $directoryList" + ) + } + } + + /** + * Consumer for processing [Dump] objects and passing them to [TestCoverageBuilder]. + * + * @param logger The logger to use for logging. + * @param locationIncludeFilter The filter to use for including locations. + * @param nextConsumer The consumer to pass the generated [TestCoverageBuilder] to. + */ + inner class DumpConsumer( + private val logger: ILogger, + private val locationIncludeFilter: ClasspathWildcardIncludeFilter, + private val nextConsumer: Consumer + ) : Consumer { + override fun accept(dump: Dump) { + val testId = dump.info.id.takeIf { it.isNotEmpty() } ?: return logger.debug( + "Session with empty name detected, possibly indicating intermediate coverage." + ) + runCatching { buildCoverage(testId, dump.store, locationIncludeFilter) } + .onSuccess(nextConsumer::accept) + .onFailure { e -> logger.error("Failed to generate coverage for test $testId", e) } + } + + /** + * Builds coverage for a given test and store. + */ + private fun buildCoverage( + testId: String, + executionDataStore: ExecutionDataStore, + locationIncludeFilter: ClasspathWildcardIncludeFilter + ): TestCoverageBuilder { + val testCoverage = TestCoverageBuilder(testId) + executionDataStore.contents.forEach { executionData -> + probeCache.getCoverage(executionData, locationIncludeFilter)?.let { + testCoverage.add(it) + } + } + probeCache.flushLogger() + return testCoverage + } + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/JaCoCoTestwiseReportGenerator.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/JaCoCoTestwiseReportGenerator.kt new file mode 100644 index 000000000..bfa8df949 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/JaCoCoTestwiseReportGenerator.kt @@ -0,0 +1,116 @@ +package com.teamscale.report.testwise.jacoco + +import com.teamscale.report.EDuplicateClassFileBehavior +import com.teamscale.report.jacoco.dump.Dump +import com.teamscale.report.testwise.jacoco.CachingExecutionDataReader.* +import com.teamscale.report.testwise.jacoco.cache.CoverageGenerationException +import com.teamscale.report.testwise.model.TestwiseCoverage +import com.teamscale.report.testwise.model.builder.TestCoverageBuilder +import com.teamscale.report.util.ClasspathWildcardIncludeFilter +import com.teamscale.report.util.ILogger +import org.jacoco.core.data.ExecutionData +import org.jacoco.core.data.ExecutionDataReader +import org.jacoco.core.data.ExecutionDataStore +import org.jacoco.core.data.IExecutionDataVisitor +import org.jacoco.core.data.ISessionInfoVisitor +import org.jacoco.core.data.SessionInfo +import java.io.BufferedInputStream +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.util.function.Consumer + +/** + * Creates an XML report for an execution data store. The report is grouped by session. + * + * The class files under test must be compiled with debug information otherwise no coverage will be collected. + */ +open class JaCoCoTestwiseReportGenerator( + codeDirectoriesOrArchives: Collection, + private val locationIncludeFilter: ClasspathWildcardIncludeFilter, + duplicateClassFileBehavior: EDuplicateClassFileBehavior, + logger: ILogger +) { + /** The execution data reader and converter. */ + private val executionDataReader = CachingExecutionDataReader( + logger, codeDirectoriesOrArchives, locationIncludeFilter, duplicateClassFileBehavior + ) + + init { + // This has to be unsafe as mockito does not support mocking final classes + updateClassDirCache() + } + + /** Updates the probe cache of the [ExecutionDataReader]. */ + open fun updateClassDirCache() { + executionDataReader.analyzeClassDirs() + } + + /** Converts the given dumps to a report. */ + @Throws(IOException::class, CoverageGenerationException::class) + open fun convert(executionDataFile: File): TestwiseCoverage { + val testwiseCoverage = TestwiseCoverage() + val dumpConsumer = executionDataReader.buildCoverageConsumer(locationIncludeFilter, testwiseCoverage::add) + readAndConsumeDumps(executionDataFile, dumpConsumer) + return testwiseCoverage + } + + /** Converts the given dump to a report. */ + @Throws(CoverageGenerationException::class) + open fun convert(dump: Dump): TestCoverageBuilder? { + val testCoverageBuilders = mutableListOf() + executionDataReader + .buildCoverageConsumer(locationIncludeFilter, testCoverageBuilders::add) + .accept(dump) + return testCoverageBuilders.singleOrNull() + } + + /** Converts the given dumps to a report. */ + @Throws(IOException::class) + open fun convertAndConsume(executionDataFile: File, consumer: Consumer) { + val dumpConsumer = executionDataReader.buildCoverageConsumer(locationIncludeFilter, consumer) + readAndConsumeDumps(executionDataFile, dumpConsumer) + } + + /** Reads the dumps from the given *.exec file. */ + @Throws(IOException::class) + private fun readAndConsumeDumps(executionDataFile: File, dumpConsumer: DumpConsumer) { + BufferedInputStream(FileInputStream(executionDataFile)).use { input -> + ExecutionDataReader(input).apply { + val dumpCallback = DumpCallback(dumpConsumer) + setExecutionDataVisitor(dumpCallback) + setSessionInfoVisitor(dumpCallback) + read() + dumpCallback.processDump() + } + } + } + + /** Collects execution information per session and passes it to the consumer . */ + private class DumpCallback(private val consumer: DumpConsumer) : IExecutionDataVisitor, ISessionInfoVisitor { + /** The dump that is currently being read. */ + private var currentDump: Dump? = null + + /** The store to which coverage is currently written to. */ + private var store: ExecutionDataStore? = null + + override fun visitSessionInfo(info: SessionInfo) { + processDump() + ExecutionDataStore().let { + currentDump = Dump(info, it) + store = it + } + } + + override fun visitClassExecution(data: ExecutionData) { + store?.put(data) + } + + fun processDump() { + currentDump?.let { + consumer.accept(it) + currentDump = null + } + } + } +} \ No newline at end of file diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/AnalyzerCache.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/AnalyzerCache.kt new file mode 100644 index 000000000..6ad52eee1 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/AnalyzerCache.kt @@ -0,0 +1,72 @@ +package com.teamscale.report.testwise.jacoco.cache + +import com.teamscale.report.jacoco.FilteringAnalyzer +import com.teamscale.report.util.ClasspathWildcardIncludeFilter +import com.teamscale.report.util.ILogger +import org.jacoco.core.internal.analysis.CachingClassAnalyzer +import org.jacoco.core.internal.analysis.ClassCoverageImpl +import org.jacoco.core.internal.analysis.StringPool +import org.jacoco.core.internal.data.CRC64 +import org.jacoco.core.internal.flow.ClassProbesAdapter +import org.jacoco.core.internal.instr.InstrSupport +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import java.io.IOException +import java.io.InputStream +import java.nio.file.Files +import java.nio.file.Paths + +/** + * An [AnalyzerCache] instance processes a set of Java class/jar/war/... files and builds a cache for each of the classes. + * + * For every class that gets found, [analyzeClass] is called. A class is identified by its class ID, which + * is a CRC64 checksum of the class file. We process each class with `CachingClassAnalyzer` to fill a cache. + */ +class AnalyzerCache( + private val probesCache: ProbesCache, + locationIncludeFilter: ClasspathWildcardIncludeFilter, + logger: ILogger +) : FilteringAnalyzer(null, null, locationIncludeFilter, logger) { + private val stringPool = StringPool() + + /** + * Analyzes the given class. Instead of the original implementation in [Analyzer.analyzeClass] we + * don't use concrete execution data, but instead build a probe cache to speed up repeated lookups. + */ + override fun analyzeClass(source: ByteArray) { + val classId = CRC64.classId(source) + if (probesCache.containsClassId(classId)) { + return + } + val reader = InstrSupport.classReaderFor(source) + + // Dummy class coverage object that allows us to subclass ClassAnalyzer with CachingClassAnalyzer and reuse its + // IFilterContext implementation + val dummyClassCoverage = ClassCoverageImpl( + reader.className, classId, false + ) + + val classAnalyzer = CachingClassAnalyzer( + probesCache.createClass(classId, reader.className), + dummyClassCoverage, + stringPool + ) + val visitor = ClassProbesAdapter(classAnalyzer, false) + reader.accept(visitor, 0) + } + + /** + * Adds caching for jar files to the analyze jar functionality. + */ + @Throws(IOException::class) + override fun analyzeJar(input: InputStream, location: String): Int { + val jarId = CRC64.classId(Files.readAllBytes(Paths.get(location))) + val probesCountForJarId = probesCache.countForJarId(jarId) + if (probesCountForJarId != 0) { + return probesCountForJarId + } + val count = super.analyzeJar(input, location) + probesCache.addJarId(jarId, count) + return count + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/ClassCoverageLookup.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/ClassCoverageLookup.kt new file mode 100644 index 000000000..ac01714f2 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/ClassCoverageLookup.kt @@ -0,0 +1,73 @@ +package com.teamscale.report.testwise.jacoco.cache + +import com.teamscale.client.StringUtils +import com.teamscale.report.testwise.model.builder.FileCoverageBuilder +import com.teamscale.report.util.CompactLines +import com.teamscale.report.util.ILogger +import org.jacoco.core.data.ExecutionData + +/** + * Holds information about a class' probes and to which line ranges they refer. + * + * Create an instance of this class for every analyzed java class. + * Set the file name of the java source file from which the class has been created. + * Then call [addProbe] for all probes and lines that belong to that probe. + * Afterward call [getFileCoverage] to transform probes ([ExecutionData]) for this class into covered lines + * ([FileCoverageBuilder]). + * + * @param className Classname as stored in the bytecode e.g., com/company/Example + */ +class ClassCoverageLookup internal constructor( + private val className: String +) { + var sourceFileName: String? = null + private val probes = mutableMapOf() + + /** Adds the probe with the given id to the method. */ + fun addProbe(probeId: Int, lines: CompactLines) { + probes[probeId] = lines + } + + /** + * Generates [FileCoverageBuilder] from an [ExecutionData]. [ExecutionData] holds coverage of + * exactly one class (whereby inner classes are a separate class). This method returns a [FileCoverageBuilder] + * object which is later merged with the [FileCoverageBuilder] of other classes that reside in the same file. + */ + @Throws(CoverageGenerationException::class) + fun getFileCoverage(executionData: ExecutionData, logger: ILogger): FileCoverageBuilder? { + val executedProbes = executionData.probes + + when { + probes.size > executedProbes.size -> throw CoverageGenerationException( + "Probe lookup does not match with actual probe size for $sourceFileName $className (${probes.size} vs ${executedProbes.size})! This is a bug in the profiler tooling. Please report it back to CQSE." + ) + sourceFileName == null -> { + logger.warn("No source file name found for class $className! This class was probably not compiled with debug information enabled!") + return null + } + } + + val packageName = if (className.contains("/")) StringUtils.removeLastPart(className, '/') else "" + return FileCoverageBuilder(packageName, sourceFileName!!).apply { + fillFileCoverage(this, executedProbes, logger) + } + } + + private fun fillFileCoverage(fileCoverage: FileCoverageBuilder, executedProbes: BooleanArray, logger: ILogger) { + probes.forEach { (probeId, coveredLines) -> + if (executedProbes.getOrNull(probeId) == true) { + when { + coveredLines.isEmpty() -> logger.debug( + "$sourceFileName $className contains a method with no line information. Does the class contain debug information?" + ) + else -> fileCoverage.addLines(coveredLines) + } + } else { + logger.info( + "$sourceFileName $className contains a covered probe $probeId that could not be matched to any method. " + + "This could be a bug in the profiler tooling. Please report it back to CQSE." + ) + } + } + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/ClassNotFoundLogger.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/ClassNotFoundLogger.kt new file mode 100644 index 000000000..5e9edb647 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/ClassNotFoundLogger.kt @@ -0,0 +1,40 @@ +package com.teamscale.report.testwise.jacoco.cache + +import com.teamscale.report.util.ILogger + +/** + * Coordinates logging of missing class files to ensure the warnings are only emitted once and not for every individual + * test. + */ +internal class ClassNotFoundLogger( + private val logger: ILogger +) { + /** Missing classes that will be logged when [.flush] is called. */ + private val classesToBeLogged = hashSetOf() + + /** Classes that have already been reported as missing. */ + private val alreadyLoggedClasses = hashSetOf() + + /** Saves the given class to be logged later on. Ensures that the class is only logged once. */ /* package */ + fun log(fullyQualifiedClassName: String) { + if (alreadyLoggedClasses.contains(fullyQualifiedClassName)) return + classesToBeLogged.add(fullyQualifiedClassName) + } + + /** Writes a summary of the missing class files to the logger. */ /* package */ + fun flush() { + if (classesToBeLogged.isEmpty()) return + + logger.warn( + "Found coverage for " + classesToBeLogged + .size + " classes that were not provided. Either you did not provide " + + "all relevant class files or you did not adjust the include/exclude filters on the agent to exclude " + + "coverage from irrelevant code. The classes are:" + ) + classesToBeLogged.forEach { fullyQualifiedClassName -> + logger.warn(" - $fullyQualifiedClassName") + } + alreadyLoggedClasses.addAll(classesToBeLogged) + classesToBeLogged.clear() + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/CoverageGenerationException.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/CoverageGenerationException.kt new file mode 100644 index 000000000..69cf63a56 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/CoverageGenerationException.kt @@ -0,0 +1,6 @@ +package com.teamscale.report.testwise.jacoco.cache + +/** + * Exception thrown during coverage generation. + */ +class CoverageGenerationException(message: String) : Exception(message) diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/ProbesCache.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/ProbesCache.kt new file mode 100644 index 000000000..a6dcca6ef --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/ProbesCache.kt @@ -0,0 +1,97 @@ +package com.teamscale.report.testwise.jacoco.cache + +import com.teamscale.report.EDuplicateClassFileBehavior +import com.teamscale.report.testwise.model.builder.FileCoverageBuilder +import com.teamscale.report.util.ClasspathWildcardIncludeFilter +import com.teamscale.report.util.ILogger +import org.jacoco.core.data.ExecutionData +import org.jacoco.report.JavaNames + +/** + * Holds [ClassCoverageLookup]s for all analyzed classes. + * + * @param logger The logger to use for logging. + * @param duplicateClassFileBehavior Whether to ignore non-identical duplicates of class files. + */ +class ProbesCache( + private val logger: ILogger, + private val duplicateClassFileBehavior: EDuplicateClassFileBehavior +) { + /** A mapping from class ID (CRC64 of the class file) to [ClassCoverageLookup]. */ + private val classCoverageLookups = hashMapOf() + + /** Holds all fully qualified class names that are already contained in the cache. */ + private val containedClasses = mutableSetOf() + private val containedJars = mutableMapOf() + private val classNotFoundLogger = ClassNotFoundLogger(logger) + + /** Adds a new class entry to the cache and returns its [ClassCoverageLookup]. */ + fun createClass(classId: Long, className: String): ClassCoverageLookup { + if (containedClasses.contains(className)) { + if (duplicateClassFileBehavior != EDuplicateClassFileBehavior.IGNORE) { + logger.warn( + "Non-identical class file for class $className. This happens when a class with the " + + "same fully-qualified name is loaded twice but the two loaded class files are not identical. " + + "A common reason for this is that the same library or shared code is included twice in your " + + "application but in two different versions. The produced coverage for this class may not be " + + "accurate or may even be unusable. To fix this problem, please resolve the conflict between " + + "both class files in your application." + ) + } + check(duplicateClassFileBehavior != EDuplicateClassFileBehavior.FAIL) { "Found non-identical class file for class $className. See logs for more details." } + } + containedClasses.add(className) + val classCoverageLookup = ClassCoverageLookup(className) + classCoverageLookups[classId] = classCoverageLookup + return classCoverageLookup + } + + /** Returns whether a class with the given class ID has already been analyzed. */ + fun containsClassId(classId: Long) = + classCoverageLookups.containsKey(classId) + + /** + * Returns the number of found class files in a cached jar file. Otherwise 0. + */ + fun countForJarId(jarId: Long) = + containedJars.getOrDefault(jarId, 0) + + /** + * Adds a jar id along with the count of class files found in the jar. + */ + fun addJarId(jarId: Long, count: Int) { + containedJars[jarId] = count + } + + /** + * Converts the given [ExecutionData] to [FileCoverageBuilder] using the cached lookups or null if the + * class file of this class has not been included in the analysis or was not covered. + */ + @Throws(CoverageGenerationException::class) + fun getCoverage( + executionData: ExecutionData, + locationIncludeFilter: ClasspathWildcardIncludeFilter + ): FileCoverageBuilder? { + val classId = executionData.id + if (!containsClassId(classId)) { + val fullyQualifiedClassName = JavaNames().getQualifiedClassName(executionData.name) + if (locationIncludeFilter.isIncluded("$fullyQualifiedClassName.class")) { + classNotFoundLogger.log(fullyQualifiedClassName) + } + return null + } + if (!executionData.hasHits()) { + return null + } + + return classCoverageLookups[classId]?.getFileCoverage(executionData, logger) + } + + /** Returns true if the cache does not contain coverage for any class. */ + val isEmpty: Boolean + get() = classCoverageLookups.isEmpty() + + fun flushLogger() { + classNotFoundLogger.flush() + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/ERevisionType.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/ERevisionType.kt new file mode 100644 index 000000000..566aa148a --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/ERevisionType.kt @@ -0,0 +1,10 @@ +package com.teamscale.report.testwise.model + +/** Type of revision information. */ +enum class ERevisionType { + /** Commit descriptor in the format branch:timestamp. */ + COMMIT, + + /** Source control revision, e.g. SVN revision or Git hash. */ + REVISION +} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/ETestExecutionResult.java b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/ETestExecutionResult.kt similarity index 82% rename from report-generator/src/main/java/com/teamscale/report/testwise/model/ETestExecutionResult.java rename to report-generator/src/main/kotlin/com/teamscale/report/testwise/model/ETestExecutionResult.kt index 034082081..1c9497ccf 100644 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/ETestExecutionResult.java +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/ETestExecutionResult.kt @@ -15,23 +15,22 @@ | limitations under the License. | | | +-------------------------------------------------------------------------*/ -package com.teamscale.report.testwise.model; +package com.teamscale.report.testwise.model -/** The result of a test execution. */ -public enum ETestExecutionResult { - - /** Test execution was successful. */ +/** The result of a test execution. */ +enum class ETestExecutionResult { + /** Test execution was successful. */ PASSED, - /** The test is currently marked as "do not execute" (e.g. JUnit @Ignore). */ + /** The test is currently marked as "do not execute" (e.g. JUnit @Ignore). */ IGNORED, - /** Caused by a failing assumption. */ + /** Caused by a failing assumption. */ SKIPPED, - /** Caused by a failing assertion. */ + /** Caused by a failing assertion. */ FAILURE, - /** Caused by an error during test execution (e.g. exception thrown in the test runner code, not the test itself). */ + /** Caused by an error during test execution (e.g. exception thrown in the test runner code, not the test itself). */ ERROR } diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/FileCoverage.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/FileCoverage.kt new file mode 100644 index 000000000..68d8ddd1d --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/FileCoverage.kt @@ -0,0 +1,12 @@ +package com.teamscale.report.testwise.model + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty + +/** Holds coverage of a single file. */ +class FileCoverage @JsonCreator constructor( + /** The name of the file. */ + @JvmField @param:JsonProperty("fileName") val fileName: String, + /** A list of line ranges that have been covered. */ + @JvmField @param:JsonProperty("coveredLines") val coveredLines: String +) diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/LineRange.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/LineRange.kt new file mode 100644 index 000000000..e8ebb3b82 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/LineRange.kt @@ -0,0 +1,18 @@ +package com.teamscale.report.testwise.model + +/** Holds a line range with start and end (both inclusive and 1-based). */ +data class LineRange( + private val start: Int, + var end: Int +) { + /** + * Returns the line range as used in the XML report. + * A range is returned as e.g. 2-5 or simply 3 if the start and end are equal. + */ + override fun toString() = + if (start == end) { + start.toString() + } else { + "$start-$end" + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/PathCoverage.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/PathCoverage.kt new file mode 100644 index 000000000..0f87fdeb7 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/PathCoverage.kt @@ -0,0 +1,12 @@ +package com.teamscale.report.testwise.model + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty + +/** Container for [FileCoverage]s of the same path. */ +class PathCoverage @JsonCreator constructor( + /** File system path. */ + @param:JsonProperty("path") val path: String?, + /** Files with coverage. */ + @JvmField @param:JsonProperty("files") val files: List +) diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/RevisionInfo.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/RevisionInfo.kt new file mode 100644 index 000000000..87a2a8ae8 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/RevisionInfo.kt @@ -0,0 +1,35 @@ +package com.teamscale.report.testwise.model + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import com.teamscale.client.CommitDescriptor +import java.io.Serializable + +/** Revision information necessary for uploading reports to Teamscale. */ +class RevisionInfo : Serializable { + /** The type of revision information. */ + val type: ERevisionType + + /** The value. Either a commit descriptor or a source control revision, depending on [type]. */ + val value: String? + + @JsonCreator + constructor(@JsonProperty("type") type: ERevisionType, @JsonProperty("value") value: String) { + this.type = type + this.value = value + } + + /** + * Constructor in case you have both fields, and either may be null. If both are set, the commit wins. If both are + * null, [type] will be [ERevisionType.REVISION] and [value] will be null. + */ + constructor(commit: CommitDescriptor?, revision: String?) { + if (commit == null) { + type = ERevisionType.REVISION + value = revision + } else { + type = ERevisionType.COMMIT + value = commit.toString() + } + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestExecution.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestExecution.kt new file mode 100644 index 000000000..788165a64 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestExecution.kt @@ -0,0 +1,50 @@ +/*-------------------------------------------------------------------------+ +| | +| Copyright (c) 2005-2018 The ConQAT Project | +| | +| Licensed under the Apache License, Version 2.0 (the "License"); | +| you may not use this file except in compliance with the License. | +| You may obtain a copy of the License at | +| | +| http://www.apache.org/licenses/LICENSE-2.0 | +| | +| Unless required by applicable law or agreed to in writing, software | +| distributed under the License is distributed on an "AS IS" BASIS, | +| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | +| See the License for the specific language governing permissions and | +| limitations under the License. | +| | ++-------------------------------------------------------------------------*/ +package com.teamscale.report.testwise.model + +import com.fasterxml.jackson.annotation.JsonAlias +import com.fasterxml.jackson.annotation.JsonProperty +import java.io.Serializable + +/** + * Representation of a single test (method) execution. + * + * @param uniformPath The uniform path of the test (method) that was executed. + * This is an absolute reference that identifies the test uniquely within the Teamscale project. + * @param durationMillis Duration of the execution in milliseconds. + * @param result The result of the test execution. + * @param message Optional message given for test failures (normally contains a stack trace). May be `null`. + */ +data class TestExecution @JvmOverloads constructor( + @JvmField + var uniformPath: String? = null, + @Deprecated("Use durationSeconds instead.") + var durationMillis: Long = 0L, + @JvmField + val result: ETestExecutionResult? = null, + val message: String? = null, +) : Serializable { + + /** Duration of the execution in seconds. */ + @JsonProperty("duration") + @JsonAlias("durationSeconds") + private val duration: Double? = null + + val durationSeconds: Double + get() = duration ?: (durationMillis / 1000.0) +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestInfo.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestInfo.kt new file mode 100644 index 000000000..5f0efd1c4 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestInfo.kt @@ -0,0 +1,33 @@ +package com.teamscale.report.testwise.model + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty + +/** Generic container of all information about a specific test as written to the report. */ +class TestInfo @JsonCreator constructor( + /** Unique name of the test case by using a path like hierarchical description, which can be shown in the UI. */ + @JvmField @param:JsonProperty("uniformPath") val uniformPath: String, + /** + * Path to the source of the method. Will be equal to uniformPath in most cases, but e.g. @Test methods in a Base + * class will have the sourcePath pointing to the Base class which contains the actual implementation whereas + * uniformPath will contain the class name of the most specific subclass, from where it was actually executed. + */ + @param:JsonProperty("sourcePath") val sourcePath: String?, + /** + * Some kind of content to tell whether the test specification has changed. Can be revision number or hash over the + * specification or similar. + */ + @param:JsonProperty("content") val content: String?, + /** Duration of the execution in seconds. */ + @param:JsonProperty("duration") val duration: Double?, + /** The actual execution result state. */ + @JvmField @param:JsonProperty("result") val result: ETestExecutionResult?, + /** + * Optional message given for test failures (normally contains a stack trace). May be `null`. + */ + @param:JsonProperty("message") val message: String? +) { + /** All paths that the test did cover. */ + @JvmField + val paths = mutableListOf() +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestwiseCoverage.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestwiseCoverage.kt new file mode 100644 index 000000000..1f7871522 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestwiseCoverage.kt @@ -0,0 +1,32 @@ +package com.teamscale.report.testwise.model + +import com.teamscale.report.testwise.model.builder.TestCoverageBuilder + +/** Container for coverage produced by multiple tests. */ +class TestwiseCoverage { + /** A mapping from test ID to [TestCoverageBuilder]. */ + val tests = mutableMapOf() + + /** + * Adds the [TestCoverageBuilder] to the map. + * If there is already a test with the same ID the coverage is merged. + */ + fun add(coverage: TestCoverageBuilder?) { + if (coverage == null || coverage.isEmpty) return + if (tests.containsKey(coverage.uniformPath)) { + tests[coverage.uniformPath]?.addAll(coverage.files) + } else { + tests[coverage.uniformPath] = coverage + } + } + + /** + * Merges the given [TestwiseCoverage] with this one. + */ + fun add(testwiseCoverage: TestwiseCoverage?) { + if (testwiseCoverage == null) return + testwiseCoverage.tests.values.forEach { value -> + add(value) + } + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestwiseCoverageReport.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestwiseCoverageReport.kt new file mode 100644 index 000000000..b2053f810 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestwiseCoverageReport.kt @@ -0,0 +1,17 @@ +package com.teamscale.report.testwise.model + +import com.fasterxml.jackson.annotation.JsonProperty + +/** Container for coverage produced by multiple tests. */ +data class TestwiseCoverageReport( + /** + * If set to `true` the set of tests contained in the report don't represent the full set of tests within a + * partition. These tests are added or updated in Teamscale, but no tests or executable units that are missing in + * the report will be deleted. + */ + @JvmField @param:JsonProperty("partial") val partial: Boolean +) { + /** The tests contained in the report. */ + @JvmField + val tests = mutableListOf() +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/FileCoverageBuilder.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/FileCoverageBuilder.kt new file mode 100644 index 000000000..8f3fd7b38 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/FileCoverageBuilder.kt @@ -0,0 +1,66 @@ +package com.teamscale.report.testwise.model.builder + +import com.teamscale.report.testwise.model.FileCoverage +import com.teamscale.report.testwise.model.LineRange +import com.teamscale.report.util.CompactLines +import com.teamscale.report.util.CompactLines.Companion.compactLinesOf + +/** Holds coverage of a single file. */ +class FileCoverageBuilder( + /** The file system path of the file not including the file itself. */ + val path: String, + /** The name of the file. */ + val fileName: String +) { + /** + * A set of line numbers that have been covered. Ensures order and uniqueness. + */ + private val coveredLines = compactLinesOf() + + /** Adds a line as covered. */ + fun addLine(line: Int) = coveredLines.add(line) + + /** Adds a line range as covered. */ + fun addLineRange(start: Int, end: Int) = (start..end).forEach { coveredLines.add(it) } + + /** Adds a set of lines as covered. */ + fun addLines(lines: CompactLines) = coveredLines merge lines + + /** Merges the coverage of another [FileCoverageBuilder] into the current list. */ + fun merge(other: FileCoverageBuilder) { + require(other.fileName == fileName && other.path == path) { + "Cannot merge coverage of two different files! This is a bug!" + } + coveredLines merge other.coveredLines + } + + /** + * Returns a compact string representation of the covered lines. Continuous line ranges are merged to ranges and + * sorted. Individual ranges are separated by commas. E.g. 1-5,7,9-11. + */ + fun computeCompactifiedRangesAsString(): String = + compactifyToRanges(coveredLines).joinToString(",") + + /** Returns true if there is no coverage for the file yet. */ + val isEmpty: Boolean get() = coveredLines.isEmpty() + + /** Builds the [FileCoverage] object, which is serialized into the report. */ + fun build(): FileCoverage = FileCoverage(fileName, computeCompactifiedRangesAsString()) + + companion object { + /** + * Merges all neighboring line numbers to ranges. E.g. a list of [[1-5],[3-7],[8-10],[12-14]] becomes + * [[1-10],[12-14]] + */ + @JvmStatic + fun compactifyToRanges(lines: CompactLines): List = + lines.fold(mutableListOf()) { ranges, line -> + if (ranges.isNotEmpty() && ranges.last().end >= line - 1) { + ranges.last().end = line + } else { + ranges.add(LineRange(line, line)) + } + ranges + } + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/PathCoverageBuilder.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/PathCoverageBuilder.kt new file mode 100644 index 000000000..e73dc2d42 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/PathCoverageBuilder.kt @@ -0,0 +1,37 @@ +package com.teamscale.report.testwise.model.builder + +import com.teamscale.report.testwise.model.PathCoverage + +/** Container for [FileCoverageBuilder]s of the same path. + * + * @param path File system path. + */ +class PathCoverageBuilder( + val path: String +) { + /** Mapping from file names to [FileCoverageBuilder]. */ + private val fileCoverageList = mutableMapOf() + + /** + * Adds the given [FileCoverageBuilder] to the container. + * If coverage for the same file already exists, it gets merged. + */ + fun add(fileCoverage: FileCoverageBuilder) { + fileCoverageList.merge(fileCoverage.fileName, fileCoverage) { existing, new -> + existing.apply { merge(new) } + } + } + + /** Returns a collection of [FileCoverageBuilder]s associated with this path. */ + val files: Collection + get() = fileCoverageList.values + + /** Builds a [PathCoverage] object. */ + fun build() = + PathCoverage( + path, + fileCoverageList.values + .sortedBy { it.fileName } + .map { it.build() } + ) +} \ No newline at end of file diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/TestCoverageBuilder.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/TestCoverageBuilder.kt new file mode 100644 index 000000000..a46158290 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/TestCoverageBuilder.kt @@ -0,0 +1,41 @@ +package com.teamscale.report.testwise.model.builder + +import com.teamscale.report.testwise.model.PathCoverage + +/** + * Generic holder of test coverage of a single test based on line-ranges. + * @param uniformPath The uniformPath of the test (see TEST_IMPACT_ANALYSIS_DOC.md for more information). + */ +class TestCoverageBuilder(val uniformPath: String) { + /** Mapping from path names to all files on this path. */ + private val pathCoverageList = mutableMapOf() + + /** Collection of [PathCoverageBuilder]s associated with the test. */ + val paths: List + get() = pathCoverageList.values + .sortedBy { it.path } + .map { it.build() } + + /** Adds the [FileCoverageBuilder] to into the map, but filters out file coverage that is null or empty. */ + fun add(fileCoverage: FileCoverageBuilder) { + fileCoverage.takeIf { !it.isEmpty }?.let { coverage -> + pathCoverageList.computeIfAbsent(coverage.path) { PathCoverageBuilder(it) } + .add(coverage) + } + } + + /** Adds the [FileCoverageBuilder]s into the map, but filters out empty ones. */ + fun addAll(fileCoverageList: List) { + fileCoverageList.forEach { add(it) } + } + + /** Returns all [FileCoverageBuilder]s stored for the test. */ + val files: List + get() = pathCoverageList.values + .flatMap { it.files } + .toList() + + /** Returns true if there is no coverage for the test yet. */ + val isEmpty: Boolean + get() = pathCoverageList.isEmpty() +} \ No newline at end of file diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/TestInfoBuilder.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/TestInfoBuilder.kt new file mode 100644 index 000000000..dc8c1c4d5 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/TestInfoBuilder.kt @@ -0,0 +1,67 @@ +package com.teamscale.report.testwise.model.builder + +import com.teamscale.client.TestDetails +import com.teamscale.report.testwise.model.ETestExecutionResult +import com.teamscale.report.testwise.model.TestExecution +import com.teamscale.report.testwise.model.TestInfo + +/** Generic container of all information about a specific test including details, execution info and coverage. + * @param uniformPath Unique name of the test case by using a path like hierarchical description, which can be shown in the UI. + */ +class TestInfoBuilder(val uniformPath: String) { + /** + * Path to the source of the method. Will be equal to uniformPath in most cases, but e.g. @Test methods in a base + * class will have the sourcePath pointing to the base class which contains the actual implementation whereas + * uniformPath will contain the class name of the most specific subclass, from where it was actually executed. + */ + private var sourcePath: String? = null + + /** + * Some kind of content to tell whether the test specification has changed. Can be revision number or hash over the + * specification or similar. + */ + private var content: String? = null + + /** Duration of the execution in milliseconds. */ + private var durationSeconds: Double? = null + + /** The actual execution result state. */ + private var result: ETestExecutionResult? = null + + /** + * Optional message given for test failures (normally contains a stack trace). May be `null`. + */ + private var message: String? = null + + /** Coverage generated by this test. */ + private var coverage: TestCoverageBuilder? = null + + /** Returns true if there is no coverage for the test yet. */ + val isEmpty: Boolean + get() = coverage?.isEmpty == true + + /** Sets the test details fields. */ + fun setDetails(details: TestDetails) { + sourcePath = details.sourcePath + content = details.content + } + + /** Sets the test execution fields. */ + fun setExecution(execution: TestExecution) { + durationSeconds = execution.durationSeconds + result = execution.result + message = execution.message + } + + fun setCoverage(coverage: TestCoverageBuilder) { + this.coverage = coverage + } + + /** Builds a [TestInfo] object of the data in this container. */ + fun build() = + TestInfo( + uniformPath, sourcePath, content, durationSeconds, result, message + ).apply { + paths.addAll(coverage?.paths ?: emptyList()) + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/TestwiseCoverageReportBuilder.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/TestwiseCoverageReportBuilder.kt new file mode 100644 index 000000000..ca73fb3e6 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/TestwiseCoverageReportBuilder.kt @@ -0,0 +1,68 @@ +package com.teamscale.report.testwise.model.builder + +import com.teamscale.client.TestDetails +import com.teamscale.report.testwise.model.TestExecution +import com.teamscale.report.testwise.model.TestInfo +import com.teamscale.report.testwise.model.TestwiseCoverageReport +import java.util.function.Function + +/** Container for coverage produced by multiple tests. */ +class TestwiseCoverageReportBuilder { + /** A mapping from test ID to [TestCoverageBuilder]. */ + private val tests = mutableMapOf() + + private fun build(partial: Boolean): TestwiseCoverageReport { + val report = TestwiseCoverageReport(partial) + tests.values + .sortedBy { it.uniformPath } + .map { it.build() } + .forEach { report.tests.add(it) } + return report + } + + companion object { + /** + * Adds the [TestCoverageBuilder] to the map. If there is already a test with the same ID the coverage is + * merged. + */ + @JvmStatic + fun createFrom( + testDetailsList: Collection, + testCoverage: Collection, + testExecutions: Collection, + partial: Boolean + ): TestwiseCoverageReport { + val report = TestwiseCoverageReportBuilder() + testDetailsList.forEach { testDetails -> + TestInfoBuilder(testDetails.uniformPath).also { + it.setDetails(testDetails) + report.tests[testDetails.uniformPath] = it + } + } + testCoverage.forEach { coverage -> + resolveUniformPath(report, coverage.uniformPath)?.setCoverage(coverage) + } + testExecutions.forEach { testExecution -> + val path = testExecution.uniformPath ?: return@forEach + resolveUniformPath(report, path)?.setExecution(testExecution) + } + return report.build(partial) + } + + private fun resolveUniformPath(report: TestwiseCoverageReportBuilder, uniformPath: String) = + if (report.tests.containsKey(uniformPath)) { + report.tests[uniformPath] + } else { + val shortenedUniformPath = stripParameterizedTestArguments(uniformPath) + report.tests[shortenedUniformPath] + } ?: run { + System.err.println("No container found for test '$uniformPath'!"); null + } + + /** + * Removes parameterized test arguments from the given uniform path. + */ + fun stripParameterizedTestArguments(uniformPath: String) = + uniformPath.replaceFirst("(.*\\))\\[.*]".toRegex(), "$1") + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/factory/TestInfoFactory.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/factory/TestInfoFactory.kt new file mode 100644 index 000000000..f26c3dc51 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/factory/TestInfoFactory.kt @@ -0,0 +1,83 @@ +package com.teamscale.report.testwise.model.factory + +import com.teamscale.client.TestDetails +import com.teamscale.report.testwise.model.TestExecution +import com.teamscale.report.testwise.model.TestInfo +import com.teamscale.report.testwise.model.builder.TestCoverageBuilder +import com.teamscale.report.testwise.model.builder.TestInfoBuilder +import com.teamscale.report.testwise.model.builder.TestwiseCoverageReportBuilder + +/** + * Factory class for converting [TestCoverageBuilder] to [TestInfo]s while augmenting them with information + * from test details and test executions. + */ +class TestInfoFactory(testDetails: List, testExecutions: List) { + /** Maps uniform paths to test details. */ + private val testDetailsMap = mutableMapOf() + + /** Maps uniform paths to test executions. */ + private val testExecutionsMap = mutableMapOf() + + /** Holds all uniform paths for tests that have been written to the outputFile. */ + private val processedTestUniformPaths = mutableSetOf() + + init { + testDetails.forEach { testDetail -> + testDetailsMap[testDetail.uniformPath] = testDetail + } + testExecutions.forEach { testExecution -> + testExecution.uniformPath?.let { + testExecutionsMap[it] = testExecution + } + } + } + + /** + * Converts the given [TestCoverageBuilder] to a [TestInfo] using the internally stored test details and + * test executions. + */ + fun createFor(testCoverageBuilder: TestCoverageBuilder): TestInfo { + val resolvedUniformPath = testCoverageBuilder.uniformPath.resolveUniformPath() + processedTestUniformPaths.add(resolvedUniformPath) + + return TestInfoBuilder(resolvedUniformPath).apply { + setCoverage(testCoverageBuilder) + testDetailsMap[resolvedUniformPath]?.let { testDetails -> + setDetails(testDetails) + } ?: System.err.println("No test details found for $resolvedUniformPath") + testExecutionsMap[resolvedUniformPath]?.let { execution -> + setExecution(execution) + } ?: System.err.println("No test execution found for $resolvedUniformPath") + }.build() + } + + /** Returns [TestInfo]s for all tests that have not been used yet in [.createFor]. */ + fun createTestInfosWithoutCoverage(): List { + val results = testDetailsMap.values.mapNotNull { testDetails -> + if (processedTestUniformPaths.contains(testDetails.uniformPath)) return@mapNotNull null + + processedTestUniformPaths.add(testDetails.uniformPath) + TestInfoBuilder(testDetails.uniformPath).apply { + setDetails(testDetails) + testExecutionsMap[testDetails.uniformPath]?.let { setExecution(it) } + }.build() + } + testExecutionsMap.values.forEach { testExecution -> + if (processedTestUniformPaths.contains(testExecution.uniformPath)) return@forEach + System.err.println( + "Test " + testExecution.uniformPath + " was executed but no coverage was found. " + + "Please make sure that you did provide all relevant exec files and that the test IDs passed to " + + "the agent match the ones from the provided test execution list." + ) + testExecution.uniformPath?.let { processedTestUniformPaths.add(it) } + } + return results + } + + /** + * Strips parameterized test arguments when the full path given in the coverage file cannot be found in the test + * details. + */ + private fun String.resolveUniformPath() = + testDetailsMap[this]?.uniformPath ?: TestwiseCoverageReportBuilder.stripParameterizedTestArguments(this) +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/util/AntPatternIncludeFilter.kt b/report-generator/src/main/kotlin/com/teamscale/report/util/AntPatternIncludeFilter.kt new file mode 100644 index 000000000..2ba781fd5 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/util/AntPatternIncludeFilter.kt @@ -0,0 +1,48 @@ +/*-------------------------------------------------------------------------+ +| | +| Copyright (c) 2009-2018 CQSE GmbH | +| | ++-------------------------------------------------------------------------*/ +package com.teamscale.report.util + +import com.teamscale.client.AntPatternUtils +import com.teamscale.client.FileSystemUtils +import java.util.function.Predicate +import java.util.regex.Pattern +import java.util.stream.Collectors + +/** + * Applies ANT include and exclude patterns to paths. + */ +class AntPatternIncludeFilter( + locationIncludeFilters: List, + locationExcludeFilters: List +) : Predicate { + /** The include filters. Empty means include everything. */ + private val locationIncludeFilters: List = + locationIncludeFilters.map { AntPatternUtils.convertPattern(it, false) } + + /** The exclude filters. Empty means exclude nothing. */ + private val locationExcludeFilters: List = + locationExcludeFilters.map { AntPatternUtils.convertPattern(it, false) } + + /** {@inheritDoc} */ + override fun test(path: String) = !isFiltered(FileSystemUtils.normalizeSeparators(path)) + + /** + * Returns `true` if the given class file location (normalized to forward slashes as path separators) + * should not be analyzed. + * + * + * Exclude filters overrule include filters. + */ + private fun isFiltered(location: String): Boolean { + // first check includes + val noneIncludes = locationIncludeFilters.none { it.matcher(location).matches() } + if (locationIncludeFilters.isNotEmpty() && noneIncludes) { + return true + } + // only if they match, check excludes + return locationExcludeFilters.any { it.matcher(location).matches() } + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/util/BashFileSkippingInputStream.kt b/report-generator/src/main/kotlin/com/teamscale/report/util/BashFileSkippingInputStream.kt new file mode 100644 index 000000000..e2f6be389 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/util/BashFileSkippingInputStream.kt @@ -0,0 +1,67 @@ +package com.teamscale.report.util + +import java.io.BufferedInputStream +import java.io.FilterInputStream +import java.io.IOException +import java.io.InputStream + +/** + * This InputStream skips any initial bash script prepended to an executable spring-boot jar file, + * positioning the stream at the start of the ZIP file header. This allows direct execution of + * the jar without requiring "java -jar my.jar". + */ +class BashFileSkippingInputStream(input: InputStream) : FilterInputStream(BufferedInputStream(input)) { + + companion object { + private val ZIP_HEADER = byteArrayOf(0x50, 0x4B, 0x03, 0x04) + private const val BUFFER_SIZE = 8192 + } + + init { + skipToZipHeader() + } + + /** + * Reads the stream until the ZIP file header (0x50 4B 03 04) is found, or EOF is reached. + * After calling this method, the read pointer is positioned at the first byte of the ZIP header. + */ + @Throws(IOException::class) + private fun skipToZipHeader() { + val buffer = ByteArray(BUFFER_SIZE) + `in`.mark(BUFFER_SIZE) + + var bytesRead = `in`.read(buffer, 0, BUFFER_SIZE) + while (bytesRead != -1) { + val headerIndex = findZipHeader(buffer, bytesRead) + if (headerIndex != -1) { + // Reset and skip to the start of the ZIP header + `in`.reset() + `in`.skip(headerIndex.toLong()) + return + } + + // Adjust position and re-mark to check the buffer boundary for header + `in`.reset() + `in`.skip((BUFFER_SIZE - ZIP_HEADER.size + 1).toLong()) + `in`.mark(BUFFER_SIZE) + bytesRead = `in`.read(buffer, 0, BUFFER_SIZE) + } + + throw IOException("ZIP header not found in the input stream.") + } + + /** + * Searches the buffer for the ZIP header signature. + * @param buffer The buffer to search. + * @param length The number of valid bytes in the buffer. + * @return The index where the ZIP header starts, or -1 if not found. + */ + private fun findZipHeader(buffer: ByteArray, length: Int) = + (0 .. length - ZIP_HEADER.size) + .firstOrNull { + buffer[it] == ZIP_HEADER[0] + && buffer[it + 1] == ZIP_HEADER[1] + && buffer[it + 2] == ZIP_HEADER[2] + && buffer[it + 3] == ZIP_HEADER[3] + } ?: -1 +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/util/ClasspathWildcardIncludeFilter.kt b/report-generator/src/main/kotlin/com/teamscale/report/util/ClasspathWildcardIncludeFilter.kt new file mode 100644 index 000000000..7de836219 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/util/ClasspathWildcardIncludeFilter.kt @@ -0,0 +1,80 @@ +package com.teamscale.report.util + +import com.teamscale.client.FileSystemUtils +import com.teamscale.client.StringUtils +import org.jacoco.core.runtime.WildcardMatcher +import org.jacoco.report.JavaNames +import java.util.* + + +/*** + * Tests given class file paths against call name patterns. + * E.g. "/some/file/path/test.jar@my/package/Test.class" matches "my/package/ *" or "my/package/Test" + */ +open class ClasspathWildcardIncludeFilter( + locationIncludeFilters: String?, + locationExcludeFilters: String? +) { + /** + * Include patterns to apply during JaCoCo's traversal of class files. If null then everything is included. + */ + private var locationIncludeFilters: WildcardMatcher? = null + + /** + * Exclude patterns to apply during JaCoCo's traversal of class files. If null then nothing is excluded. + */ + private var locationExcludeFilters: WildcardMatcher? = null + + /** + * Constructor. + * + * @param locationIncludeFilters Colon separated list of wildcard include patterns for fully qualified class names + * or null for no includes. See [WildcardMatcher] for the pattern syntax. + * @param locationExcludeFilters Colon separated list of wildcard exclude patterns for fully qualified class names + * or null for no excludes.See [WildcardMatcher] for the pattern syntax. + */ + init { + if (!locationIncludeFilters.isNullOrEmpty()) { + this.locationIncludeFilters = WildcardMatcher(locationIncludeFilters) + } + if (!locationExcludeFilters.isNullOrEmpty()) { + this.locationExcludeFilters = WildcardMatcher(locationExcludeFilters) + } + } + + /** + * Tests if the given file path (e.g. "/some/file/path/test.jar@my/package/Test.class" or "org/mypackage/MyClass" + */ + fun isIncluded(path: String): Boolean { + val className = getClassName(path) + // first check includes + if (locationIncludeFilters != null && locationIncludeFilters?.matches(className) == false) { + return false + } + // if they match, check excludes + return locationExcludeFilters == null || locationExcludeFilters?.matches(className) == false + } + + + companion object { + /** + * Returns the normalized class name of the given class file's path. I.e. turns something like + * "/opt/deploy/some.jar@com/teamscale/Class.class" into something like "com.teamscale.Class". + */ + @JvmStatic + fun getClassName(path: String): String { + val parts = FileSystemUtils.normalizeSeparators(path) + .split("@".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + if (parts.isEmpty()) { + return "" + } + + var pathInsideJar = parts[parts.size - 1] + if (path.lowercase(Locale.getDefault()).endsWith(".class")) { + pathInsideJar = StringUtils.removeLastPart(pathInsideJar, '.') + } + return JavaNames().getQualifiedClassName(pathInsideJar) + } + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/util/CommandLineLogger.kt b/report-generator/src/main/kotlin/com/teamscale/report/util/CommandLineLogger.kt new file mode 100644 index 000000000..fe7788234 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/util/CommandLineLogger.kt @@ -0,0 +1,30 @@ +package com.teamscale.report.util + +/** Logger that prints all output to the console. */ +class CommandLineLogger : ILogger { + override fun debug(message: String) { + println(message) + } + + override fun info(message: String) { + println(message) + } + + override fun warn(message: String) { + System.err.println(message) + } + + override fun warn(message: String, throwable: Throwable?) { + System.err.println(message) + throwable?.printStackTrace() + } + + override fun error(throwable: Throwable) { + throwable.printStackTrace() + } + + override fun error(message: String, throwable: Throwable?) { + System.err.println(message) + throwable?.printStackTrace() + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/util/CompactLines.kt b/report-generator/src/main/kotlin/com/teamscale/report/util/CompactLines.kt new file mode 100644 index 000000000..9a8bfcf75 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/util/CompactLines.kt @@ -0,0 +1,212 @@ +/* + * Copyright (c) CQSE GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.teamscale.report.util + +import java.io.IOException +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.io.Serializable +import java.util.BitSet +import java.util.NoSuchElementException + +/** + * A compact, serializable representation of line numbers using a BitSet. This class is designed to + * efficiently store and manipulate sets of line numbers, which is particularly useful for tracking + * coverage information, regions of a text, or any scenario where line-based data needs to be + * compactly managed. + * + * Instances of this class can be created empty, from a collection of integers, or from a string + * representation of line number ranges. It supports basic set operations such as addition, removal, + * intersection, and union, as well as specialized operations like checking if any line number + * within a range or specific region is present. + * + * This class also implements [Iterable], allowing for easy iteration over all stored line + * numbers. + * + * @see BitSet + */ +data class CompactLines( + private var bitSet: BitSet = BitSet() +) : Serializable, Iterable { + + companion object { + private const val serialVersionUID = 1L + + fun compactLinesOf(vararg lines: Int) = CompactLines(*lines) + fun compactLinesOf(lines: Iterable) = CompactLines(lines) + fun compactLinesOf() = CompactLines() + } + + constructor(lines: Iterable) : this() { + lines.forEach { line -> + bitSet.set(line) + } + } + + constructor(vararg lines: Int) : this() { + lines.forEach { line -> + bitSet.set(line) + } + } + + /** Returns the number of line numbers in this set. */ + fun size() = bitSet.cardinality() + + /** + * Checks if this set of line numbers is empty. + * + * @return `true` if there are no line numbers in this set, `false` otherwise. + */ + fun isEmpty() = bitSet.isEmpty + + /** + * Adds all line numbers from another [CompactLines] instance to this one. + */ + infix fun merge(lines: CompactLines) { + bitSet.or(lines.bitSet) + } + + /** + * Checks if a specific line number is present in this set. + * + * @param line The line number (1-based) + * @return `true` if the line number is present, `false` otherwise. + */ + fun contains(line: Int) = bitSet.get(line) + + /** + * Checks if any line number within a specified range is present in this set. + * + * @param start the start of the range (inclusive, 1-based). + * @param end the end of the range (inclusive, 1-based). + * @return `true` if any line number within the range is present, `false` otherwise. + */ + fun containsAny(start: Int, end: Int): Boolean { + val nextSetBit = bitSet.nextSetBit(start) + return nextSetBit != -1 && nextSetBit <= end + } + + /** + * Checks if this set contains all the line numbers specified in an iterable collection. + * + * @return `true` if every line number in the collection is contained in this set, + * `false` otherwise. + */ + fun containsAll(lines: Iterable) = + lines.all { line -> bitSet.get(line) } + + /** + * Adds a specific line number to this set. + * + * @param line The line number (1-based) + */ + fun add(line: Int) { + bitSet.set(line) + } + + /** + * Adds a range of line numbers to this set. + * + * @param startLine the starting line number of the range to add (inclusive, 1-based) + * @param endLine the ending line number of the range to add (inclusive, 1-based) + */ + fun addRange(startLine: Int, endLine: Int) { + bitSet.set(startLine, endLine + 1) + } + + /** Removes a specific line number from this set. */ + fun remove(line: Int) { + bitSet.clear(line) + } + + /** + * Removes all line numbers that are present in another [CompactLines] instance from this one. + */ + fun removeAll(lines: CompactLines) { + bitSet.andNot(lines.bitSet) + } + + /** Clears all line numbers from this set. */ + fun clear() { + bitSet.clear() + } + + /** + * Retains only the line numbers that are present in both this and another [CompactLines] + * instance. This basically builds the intersection set between both. + */ + fun retainAll(lines: CompactLines) { + bitSet.and(lines.bitSet) + } + + /** + * Creates a new [CompactLines] object with the intersection of this and the other lines. + */ + fun intersection(other: CompactLines) = + compactLinesOf(this).apply { + retainAll(other) + } + + /** + * Checks if there is any overlap between the line numbers in this and another [CompactLines] + * instance. + * + * @return `true` if there is at least one common line number, `false` otherwise. + */ + fun intersects(lines: CompactLines) = + bitSet.intersects(lines.bitSet) + + override fun toString() = joinToString(",") + + /** + * Gets the highest line number contained in this set or null if there are no line numbers + * contained. + */ + fun getHighestLineNumber() = + if (bitSet.isEmpty) null else bitSet.previousSetBit(bitSet.length() - 1) + + @Throws(IOException::class) + private fun writeObject(out: ObjectOutputStream) { + val bytes = bitSet.toByteArray() + out.write(bytes) + } + + @Throws(IOException::class, ClassNotFoundException::class) + private fun readObject(`in`: ObjectInputStream) { + bitSet = BitSet.valueOf(`in`.readBytes()) + } + + override fun iterator(): Iterator { + return object : Iterator { + private var currentIndex = -1 + + override fun hasNext(): Boolean { + val nextIndex = bitSet.nextSetBit(currentIndex + 1) + return nextIndex != -1 + } + + override fun next(): Int { + if (!hasNext()) { + throw NoSuchElementException() + } + currentIndex = bitSet.nextSetBit(currentIndex + 1) + return currentIndex + } + } + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/util/ILogger.kt b/report-generator/src/main/kotlin/com/teamscale/report/util/ILogger.kt new file mode 100644 index 000000000..3af56f69f --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/util/ILogger.kt @@ -0,0 +1,27 @@ +package com.teamscale.report.util + +/** + * Minimal logging interface. + * + * We use this to work around some strange problems when using log4j from the Teamscale Gradle plugin. + */ +interface ILogger { + + /** Logs at debug level. */ + fun debug(message: String) + + /** Logs at info level. */ + fun info(message: String) + + /** Logs at warning level. */ + fun warn(message: String) + + /** Logs at warning level. The given [Throwable] may be null. */ + fun warn(message: String, throwable: Throwable?) + + /** Logs at error level. */ + fun error(throwable: Throwable) + + /** Logs at error level. The given [Throwable] may be null. */ + fun error(message: String, throwable: Throwable? = null) +} diff --git a/report-generator/src/main/kotlin/org/jacoco/core/internal/analysis/CachingClassAnalyzer.kt b/report-generator/src/main/kotlin/org/jacoco/core/internal/analysis/CachingClassAnalyzer.kt new file mode 100644 index 000000000..2c4444603 --- /dev/null +++ b/report-generator/src/main/kotlin/org/jacoco/core/internal/analysis/CachingClassAnalyzer.kt @@ -0,0 +1,45 @@ +package org.jacoco.core.internal.analysis + +import com.teamscale.report.testwise.jacoco.cache.ClassCoverageLookup +import org.jacoco.core.internal.flow.MethodProbesVisitor +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.tree.MethodNode + +/** + * Analyzes a class to reconstruct probe information. + * + * + * A probe lookup holds for a single class which probe belongs to which lines. The actual filling of the + * [ClassCoverageLookup] happens in [CachingInstructionsBuilder]. + * + * @param classCoverageLookup cache for the class' probes + * @param coverage coverage node for the analyzed class data + * @param stringPool shared pool to minimize the number of [String] instances + */ +class CachingClassAnalyzer( + private val classCoverageLookup: ClassCoverageLookup, + coverage: ClassCoverageImpl, + stringPool: StringPool +) : ClassAnalyzer(coverage, null, stringPool) { + override fun visitSource(source: String?, debug: String?) { + super.visitSource(source, debug) + classCoverageLookup.sourceFileName = source + } + + override fun visitMethod( + access: Int, name: String?, + desc: String?, signature: String?, exceptions: Array? + ): MethodProbesVisitor { + val builder = CachingInstructionsBuilder(classCoverageLookup) + + return object : MethodAnalyzer(builder) { + override fun accept( + methodNode: MethodNode, + methodVisitor: MethodVisitor + ) { + super.accept(methodNode, methodVisitor) + builder.fillCache() + } + } + } +} diff --git a/report-generator/src/test/java/com/teamscale/report/jacoco/JaCoCoXmlReportGeneratorTest.java b/report-generator/src/test/java/com/teamscale/report/jacoco/JaCoCoXmlReportGeneratorTest.java deleted file mode 100644 index 99a630d14..000000000 --- a/report-generator/src/test/java/com/teamscale/report/jacoco/JaCoCoXmlReportGeneratorTest.java +++ /dev/null @@ -1,170 +0,0 @@ -package com.teamscale.report.jacoco; - -import com.teamscale.report.EDuplicateClassFileBehavior; -import com.teamscale.report.jacoco.dump.Dump; -import com.teamscale.report.util.ClasspathWildcardIncludeFilter; -import com.teamscale.report.util.ILogger; -import com.teamscale.test.TestDataBase; -import org.jacoco.core.data.ExecutionData; -import org.jacoco.core.data.ExecutionDataStore; -import org.jacoco.core.data.SessionInfo; -import org.jacoco.core.internal.data.CRC64; -import org.junit.jupiter.api.Test; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Collections; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; - -/** Tests report generation with and without duplicate classes. */ -public class JaCoCoXmlReportGeneratorTest extends TestDataBase { - - /** Ensures that the normal case (no duplicated classes) runs without exceptions. */ - @Test - void testNormalCaseThrowsNoException() throws Exception { - try { - runGenerator("no-duplicates", EDuplicateClassFileBehavior.FAIL); - } catch (EmptyReportException e) { - // An empty report exception is thrown here because we did not configure the correct class IDs to match the - // artificial coverage generated by createDummyDump to the actual class file. Since we are only testing for - // IllegalStateExceptions resulting from duplicated, non-identical class files, and don't care about empty - // reports here. Therefore we catch this exception to avoid letting the test fail because of it. - } - } - - /** Ensures that two identical duplicate classes do not cause problems. */ - @Test - void testIdenticalClassesShouldNotThrowException() throws Exception { - try { - runGenerator("identical-duplicate-classes", EDuplicateClassFileBehavior.FAIL); - } catch (EmptyReportException e) { - // An empty report exception is thrown here because we did not configure the correct class IDs to match the - // artificial coverage generated by createDummyDump to the actual class file. Since we are only testing for - // IllegalStateExceptions resulting from duplicated, non-identical class files, and don't care about empty - // reports here. Therefore we catch this exception to avoid letting the test fail because of it. - } - } - - /** - * Ensures that two non-identical, duplicate classes cause an exception to be thrown. - */ - @Test - void testDifferentClassesWithTheSameNameShouldThrowException() { - assertThatThrownBy(() -> runGenerator("different-duplicate-classes", EDuplicateClassFileBehavior.FAIL)) - .isExactlyInstanceOf(IOException.class).hasCauseExactlyInstanceOf(IllegalStateException.class); - } - - /** - * Ensures that two non-identical, duplicate classes do not cause an exception to be thrown if the ignore-duplicates - * flag is set. - */ - @Test - void testDifferentClassesWithTheSameNameShouldNotThrowExceptionIfFlagIsSet() throws Exception { - try { - runGenerator("different-duplicate-classes", EDuplicateClassFileBehavior.IGNORE); - } catch (EmptyReportException e) { - // An empty report exception is thrown here because we did not configure the correct class IDs to match the - // artificial coverage generated by createDummyDump to the actual class file. Since we are only testing for - // IllegalStateExceptions resulting from duplicated, non-identical class files, and don't care about empty - // reports here. Therefore we catch this exception to avoid letting the test fail because of it. - } - } - - @Test - void testEmptyCoverageFileThrowsException() throws IOException { - String testFolderName = "empty-report-handling"; - long classId = calculateClassId(testFolderName, "TestClass.class"); - assertThatThrownBy(() -> runGenerator(testFolderName, EDuplicateClassFileBehavior.IGNORE, false, - new ClasspathWildcardIncludeFilter("some.package.*", null), createDummyDump(classId))) - .isExactlyInstanceOf(EmptyReportException.class); - } - - @Test - void testNonEmptyCoverageFileDoesNotThrowException() throws IOException, EmptyReportException { - String testFolderName = "empty-report-handling"; - long classId = calculateClassId(testFolderName, "TestClass.class"); - runGenerator(testFolderName, EDuplicateClassFileBehavior.IGNORE, false, - new ClasspathWildcardIncludeFilter("*", null), createDummyDump(classId)); - } - - /** Ensures that uncovered classes are removed from the report if ignore-uncovered-classes is set. */ - @Test - void testShrinking() throws Exception { - String testFolderName = "ignore-uncovered-classes"; - long classId = calculateClassId(testFolderName, "TestClass.class"); - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - - runGenerator(testFolderName, EDuplicateClassFileBehavior.FAIL, true, - new ClasspathWildcardIncludeFilter("*", null), - createDummyDump(classId)).copy(stream); - - String xmlString = stream.toString(StandardCharsets.UTF_8.name()); - assertThat(xmlString).contains("TestClass"); - assertThat(xmlString).doesNotContain("TestClassTwo"); - assertThat(xmlString).doesNotContain("ITestInterface"); - } - - /** Ensures that uncovered classes are contained in the report if ignore-uncovered-classes is not set. */ - @Test - void testNonShrinking() throws Exception { - String testFolderName = "ignore-uncovered-classes"; - long classId = calculateClassId(testFolderName, "TestClass.class"); - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - - runGenerator(testFolderName, EDuplicateClassFileBehavior.FAIL, false, - new ClasspathWildcardIncludeFilter("*", null), - createDummyDump(classId)).copy(stream); - - String xmlString = stream.toString(StandardCharsets.UTF_8.name()); - assertThat(xmlString).contains("TestClassTwo"); - } - - /** - * Creates a dummy dump with the specified class ID. The class ID can currently be calculated with {@link - * org.jacoco.core.internal.data.CRC64#classId(byte[])}. This might change in the future, as it's considered an - * implementation detail of JaCoCo (c.f. - * https://www.jacoco.org/jacoco/trunk/doc/classids.html) - */ - private static Dump createDummyDump(long classId) { - ExecutionDataStore store = new ExecutionDataStore(); - store.put(new ExecutionData(classId, "TestClass", new boolean[]{true, true, true})); - SessionInfo info = new SessionInfo("session-id", 124L, 125L); - return new Dump(info, store); - } - - /** Creates a dummy dump with an arbitrary class ID. */ - private static Dump createDummyDump() { - return createDummyDump(123); - } - - private long calculateClassId(String testFolderName, String classFileName) throws IOException { - File classFile = useTestFile(testFolderName + File.separator + classFileName); - return CRC64.classId(Files.readAllBytes(classFile.toPath())); - } - - /** Runs the report generator with default values and without ignoring uncovered classes. */ - private CoverageFile runGenerator(String testDataFolder, - EDuplicateClassFileBehavior duplicateClassFileBehavior) throws Exception, EmptyReportException { - return runGenerator(testDataFolder, duplicateClassFileBehavior, false, new ClasspathWildcardIncludeFilter(null, null), - createDummyDump()); - } - - private CoverageFile runGenerator(String testDataFolder, - EDuplicateClassFileBehavior duplicateClassFileBehavior, boolean ignoreUncoveredClasses, - ClasspathWildcardIncludeFilter filter, - Dump dump) throws IOException, EmptyReportException { - File classFileFolder = useTestFile(testDataFolder); - long currentTime = System.currentTimeMillis(); - String outputFilePath = "test-coverage-" + currentTime + ".xml"; - return new JaCoCoXmlReportGenerator(Collections.singletonList(classFileFolder), filter, - duplicateClassFileBehavior, ignoreUncoveredClasses, - mock(ILogger.class)).convert(dump, Paths.get(outputFilePath).toFile()); - } -} diff --git a/report-generator/src/test/java/com/teamscale/report/testwise/jacoco/JaCoCoTestwiseReportGeneratorTest.java b/report-generator/src/test/java/com/teamscale/report/testwise/jacoco/JaCoCoTestwiseReportGeneratorTest.java deleted file mode 100644 index 88a3f1741..000000000 --- a/report-generator/src/test/java/com/teamscale/report/testwise/jacoco/JaCoCoTestwiseReportGeneratorTest.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.teamscale.report.testwise.jacoco; - -import com.teamscale.client.TestDetails; -import com.teamscale.report.EDuplicateClassFileBehavior; -import com.teamscale.report.ReportUtils; -import com.teamscale.report.testwise.model.ETestExecutionResult; -import com.teamscale.report.testwise.model.TestExecution; -import com.teamscale.report.testwise.model.TestwiseCoverage; -import com.teamscale.report.testwise.model.TestwiseCoverageReport; -import com.teamscale.report.testwise.model.builder.TestCoverageBuilder; -import com.teamscale.report.testwise.model.builder.TestwiseCoverageReportBuilder; -import com.teamscale.report.util.ClasspathWildcardIncludeFilter; -import com.teamscale.report.util.ILogger; -import com.teamscale.test.TestDataBase; -import org.conqat.lib.commons.filesystem.FileSystemUtils; -import org.junit.jupiter.api.Test; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; - -import java.io.File; -import java.util.ArrayList; -import java.util.Collections; - -import static org.mockito.Mockito.mock; - -/** Tests for the {@link JaCoCoTestwiseReportGenerator} class. */ -public class JaCoCoTestwiseReportGeneratorTest extends TestDataBase { - - @Test - void testSmokeTestTestwiseReportGeneration() throws Exception { - String report = runReportGenerator("jacoco/cqddl/classes.zip", "jacoco/cqddl/coverage.exec"); - String expected = FileSystemUtils.readFileUTF8(useTestFile("jacoco/cqddl/report.json.expected")); - JSONAssert.assertEquals(expected, report, JSONCompareMode.STRICT); - } - - @Test - void testSampleTestwiseReportGeneration() throws Exception { - String report = runReportGenerator("jacoco/sample/classes.zip", "jacoco/sample/coverage.exec"); - String expected = FileSystemUtils.readFileUTF8(useTestFile("jacoco/sample/report.json.expected")); - JSONAssert.assertEquals(expected, report, JSONCompareMode.STRICT); - } - - @Test - void defaultPackageIsHandledAsEmptyPath() throws Exception { - String report = runReportGenerator("jacoco/default-package/classes.zip", "jacoco/default-package/coverage.exec"); - String expected = FileSystemUtils.readFileUTF8(useTestFile("jacoco/default-package/report.json.expected")); - JSONAssert.assertEquals(expected, report, JSONCompareMode.STRICT); - } - - private String runReportGenerator(String testDataFolder, String execFileName) throws Exception { - File classFileFolder = useTestFile(testDataFolder); - ClasspathWildcardIncludeFilter includeFilter = new ClasspathWildcardIncludeFilter(null, null); - TestwiseCoverage testwiseCoverage = new JaCoCoTestwiseReportGenerator( - Collections.singletonList(classFileFolder), - includeFilter, EDuplicateClassFileBehavior.IGNORE, - mock(ILogger.class)).convert(useTestFile(execFileName)); - return ReportUtils.getTestwiseCoverageReportAsString(generateDummyReportFrom(testwiseCoverage)); - } - - /** Generates a dummy coverage report object that wraps the given {@link TestwiseCoverage}. */ - public static TestwiseCoverageReport generateDummyReportFrom(TestwiseCoverage testwiseCoverage) { - ArrayList testDetails = new ArrayList<>(); - for (TestCoverageBuilder test : testwiseCoverage.getTests()) { - testDetails.add(new TestDetails(test.getUniformPath(), "/path/to/source", "content")); - } - ArrayList testExecutions = new ArrayList<>(); - for (TestCoverageBuilder test : testwiseCoverage.getTests()) { - testExecutions.add(new TestExecution(test.getUniformPath(), test.getUniformPath().length(), - ETestExecutionResult.PASSED)); - } - return TestwiseCoverageReportBuilder.createFrom(testDetails, testwiseCoverage.getTests(), testExecutions, true); - } - -} \ No newline at end of file diff --git a/report-generator/src/test/java/com/teamscale/report/testwise/model/builder/FileCoverageBuilderTest.java b/report-generator/src/test/java/com/teamscale/report/testwise/model/builder/FileCoverageBuilderTest.java deleted file mode 100644 index d76456484..000000000 --- a/report-generator/src/test/java/com/teamscale/report/testwise/model/builder/FileCoverageBuilderTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.teamscale.report.testwise.model.builder; - -import com.teamscale.report.testwise.model.LineRange; -import com.teamscale.report.util.SortedIntList; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.junit.jupiter.api.Assertions.assertEquals; - -/** Tests the {@link FileCoverageBuilder} class. */ -class FileCoverageBuilderTest { - - /** Tests the compactification algorithm for line ranges. */ - @Test - void compactifyRanges() { - SortedIntList sortedIntList = new SortedIntList(); - sortedIntList.add(1); - sortedIntList.add(3); - sortedIntList.add(4); - sortedIntList.add(6); - sortedIntList.add(7); - sortedIntList.add(10); - List result = FileCoverageBuilder.compactifyToRanges(sortedIntList); - assertThat(result).hasToString("[1, 3-4, 6-7, 10]"); - } - - /** Tests the merge of two {@link FileCoverageBuilder} objects. */ - @Test - void mergeDoesMergeRanges() { - FileCoverageBuilder fileCoverage = new FileCoverageBuilder("path", "file"); - fileCoverage.addLine(1); - fileCoverage.addLineRange(3, 4); - fileCoverage.addLineRange(7, 10); - - FileCoverageBuilder otherFileCoverage = new FileCoverageBuilder("path", "file"); - fileCoverage.addLineRange(1, 3); - fileCoverage.addLineRange(12, 14); - fileCoverage.merge(otherFileCoverage); - assertThat(fileCoverage.computeCompactifiedRangesAsString()).isEqualTo("1-4,7-10,12-14"); - } - - /** Tests that two {@link FileCoverageBuilder} objects from different files throws an exception. */ - @Test - void mergeDoesNotAllowMergeOfTwoDifferentFiles() { - FileCoverageBuilder fileCoverage = new FileCoverageBuilder("path", "file"); - fileCoverage.addLine(1); - - FileCoverageBuilder otherFileCoverage = new FileCoverageBuilder("path", "file2"); - fileCoverage.addLineRange(1, 3); - assertThatCode(() -> fileCoverage.merge(otherFileCoverage)).isInstanceOf(AssertionError.class); - } - - /** Tests the transformation from line ranges into its string representation. */ - @Test - void getRangesAsString() { - FileCoverageBuilder fileCoverage = new FileCoverageBuilder("path", "file"); - fileCoverage.addLine(1); - fileCoverage.addLineRange(3, 4); - fileCoverage.addLineRange(6, 10); - assertEquals("1,3-4,6-10", fileCoverage.computeCompactifiedRangesAsString()); - } -} \ No newline at end of file diff --git a/report-generator/src/test/java/com/teamscale/report/util/BashFileSkippingInputStreamTest.java b/report-generator/src/test/java/com/teamscale/report/util/BashFileSkippingInputStreamTest.java deleted file mode 100644 index 7e429b8a8..000000000 --- a/report-generator/src/test/java/com/teamscale/report/util/BashFileSkippingInputStreamTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.teamscale.report.util; - -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.jar.JarEntry; -import java.util.jar.JarInputStream; - -import static org.assertj.core.api.Assertions.assertThat; - -class BashFileSkippingInputStreamTest { - - @Test - void testBashFileJar() throws IOException { - ArrayList filesInJar = getEntriesFromJarFile("spring-boot-executable-example.jar"); - assertThat(filesInJar).hasSize(110); - } - - @Test - void testNormalJar() throws IOException { - ArrayList filesInJar = getEntriesFromJarFile("normal.jar"); - assertThat(filesInJar).hasSize(284); - } - - private ArrayList getEntriesFromJarFile(String resourceName) throws IOException { - InputStream inputStream = getClass().getResourceAsStream(resourceName); - BashFileSkippingInputStream bashFileSkippingInputStream = new BashFileSkippingInputStream(inputStream); - JarInputStream jarInputStream = new JarInputStream(bashFileSkippingInputStream); - JarEntry entry; - ArrayList filesInJar = new ArrayList<>(); - while ((entry = jarInputStream.getNextJarEntry()) != null) { - filesInJar.add(entry.getName()); - } - return filesInJar; - } -} diff --git a/report-generator/src/test/java/com/teamscale/report/util/ClasspathWildcardIncludeFilterTest.java b/report-generator/src/test/java/com/teamscale/report/util/ClasspathWildcardIncludeFilterTest.java deleted file mode 100644 index 95567b9c3..000000000 --- a/report-generator/src/test/java/com/teamscale/report/util/ClasspathWildcardIncludeFilterTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.teamscale.report.util; - -import org.junit.jupiter.api.Test; - -import static com.teamscale.report.util.ClasspathWildcardIncludeFilter.getClassName; -import static org.assertj.core.api.Assertions.assertThat; - -public class ClasspathWildcardIncludeFilterTest { - - /** Tests path to class name conversion. */ - @Test - void testPathToClassNameConversion() { - assertThat(getClassName("file.jar@com/foo/Bar.class")).isEqualTo("com.foo.Bar"); - assertThat(getClassName("file.jar@com/foo/Bar$Goo.class")).isEqualTo("com.foo.Bar.Goo"); - assertThat(getClassName("file1.jar@goo/file2.jar@com/foo/Bar.class")).isEqualTo("com.foo.Bar"); - assertThat(getClassName("com/foo/Bar.class")).isEqualTo("com.foo.Bar"); - assertThat(getClassName("com/foo/Bar")).isEqualTo("com.foo.Bar"); - assertThat(getClassName( - "C:\\client-daily\\client\\plugins\\com.customer.something.client_1.2.3.4.1234566778.jar@com/customer/something/SomeClass.class")) - .isEqualTo("com.customer.something.SomeClass"); - } - - - @Test - void testMatching() { - assertThat(new ClasspathWildcardIncludeFilter(null, "org.junit.*") - .isIncluded("/junit-jupiter-engine-5.1.0.jar@org/junit/jupiter/engine/Constants.class")).isFalse(); - assertThat(new ClasspathWildcardIncludeFilter(null, "org.junit.*") - .isIncluded("org/junit/platform/commons/util/ModuleUtils$ModuleReferenceScanner.class")).isFalse(); - } -} \ No newline at end of file diff --git a/report-generator/src/test/java/com/teamscale/report/util/SortedIntListTest.java b/report-generator/src/test/java/com/teamscale/report/util/SortedIntListTest.java deleted file mode 100644 index f759978a3..000000000 --- a/report-generator/src/test/java/com/teamscale/report/util/SortedIntListTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.teamscale.report.util; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -class SortedIntListTest { - - @Test - void emptyList() { - SortedIntList sortedIntList = new SortedIntList(); - assertThat(sortedIntList.isEmpty()).isTrue(); - } - - @Test - void addSorted() { - SortedIntList sortedIntList = listOf(1, 3, 4, 7, 10); - assertThat(sortedIntList.list).startsWith(1, 3, 4, 7, 10); - assertThat(sortedIntList.size()).isEqualTo(5); - } - - @Test - void addReversed() { - SortedIntList sortedIntList = listOf(6, 5, 2, 0); - assertThat(sortedIntList.list).startsWith(0, 2, 5, 6); - assertThat(sortedIntList.size()).isEqualTo(4); - } - - @Test - void add() { - SortedIntList sortedIntList = listOf(7, 4, 9, 11, 1); - assertThat(sortedIntList.list).startsWith(1, 4, 7, 9, 11); - assertThat(sortedIntList.size()).isEqualTo(5); - } - - @Test - void mergeIntoEmptyList() { - SortedIntList sortedIntList = listOf(); - sortedIntList.addAll(listOf(1, 2, 5, 8, 9)); - assertThat(sortedIntList.list).startsWith(1, 2, 5, 8, 9); - assertThat(sortedIntList.size()).isEqualTo(5); - } - - @Test - void mergeWithEmptyList() { - SortedIntList sortedIntList = listOf(1, 2, 5, 8, 9); - sortedIntList.addAll(listOf()); - assertThat(sortedIntList.list).startsWith(1, 2, 5, 8, 9); - assertThat(sortedIntList.size()).isEqualTo(5); - } - - @Test - void mergeWithOverlap() { - SortedIntList sortedIntList = listOf(1, 2, 5, 8, 9); - sortedIntList.addAll(listOf(3, 4, 5)); - assertThat(sortedIntList.list).startsWith(1, 2, 3, 4, 5, 8, 9); - assertThat(sortedIntList.size()).isEqualTo(7); - } - - private SortedIntList listOf(int... values) { - SortedIntList sortedIntList = new SortedIntList(); - for (int value : values) { - sortedIntList.add(value); - } - return sortedIntList; - } -} \ No newline at end of file diff --git a/report-generator/src/test/java/com/teamscale/test/TestDataBase.java b/report-generator/src/test/java/com/teamscale/test/TestDataBase.java deleted file mode 100644 index c54319b5b..000000000 --- a/report-generator/src/test/java/com/teamscale/test/TestDataBase.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.teamscale.test; - -import java.io.File; - -/** Base class that supports reading test-data files. */ -public class TestDataBase { - - /** Read the given test-data file in the context of the current class's package. */ - protected File useTestFile(String fileName) { - return new File(new File("test-data", getClass().getPackage().getName()), fileName); - } -} diff --git a/report-generator/src/test/kotlin/com/teamscale/report/jacoco/JaCoCoXmlReportGeneratorTest.kt b/report-generator/src/test/kotlin/com/teamscale/report/jacoco/JaCoCoXmlReportGeneratorTest.kt new file mode 100644 index 000000000..3b8495eb3 --- /dev/null +++ b/report-generator/src/test/kotlin/com/teamscale/report/jacoco/JaCoCoXmlReportGeneratorTest.kt @@ -0,0 +1,181 @@ +package com.teamscale.report.jacoco + +import com.teamscale.report.EDuplicateClassFileBehavior +import com.teamscale.report.jacoco.dump.Dump +import com.teamscale.report.util.ClasspathWildcardIncludeFilter +import com.teamscale.test.TestDataBase +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.jacoco.core.data.ExecutionData +import org.jacoco.core.data.ExecutionDataStore +import org.jacoco.core.data.SessionInfo +import org.jacoco.core.internal.data.CRC64 +import org.junit.jupiter.api.Test +import org.mockito.Mockito +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.IOException +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Paths + +/** Tests report generation with and without duplicate classes. */ +class JaCoCoXmlReportGeneratorTest : TestDataBase() { + /** Ensures that the normal case (no duplicated classes) runs without exceptions. */ + @Test + fun testNormalCaseThrowsNoException() { + try { + runGenerator("no-duplicates", EDuplicateClassFileBehavior.FAIL) + } catch (e: EmptyReportException) { + // An empty report exception is thrown here because we did not configure the correct class IDs to match the + // artificial coverage generated by createDummyDump to the actual class file. Since we are only testing for + // IllegalStateExceptions resulting from duplicated, non-identical class files, and don't care about empty + // reports here. Therefore we catch this exception to avoid letting the test fail because of it. + } + } + + /** Ensures that two identical duplicate classes do not cause problems. */ + @Test + fun testIdenticalClassesShouldNotThrowException() { + try { + runGenerator("identical-duplicate-classes", EDuplicateClassFileBehavior.FAIL) + } catch (e: EmptyReportException) { + // An empty report exception is thrown here because we did not configure the correct class IDs to match the + // artificial coverage generated by createDummyDump to the actual class file. Since we are only testing for + // IllegalStateExceptions resulting from duplicated, non-identical class files, and don't care about empty + // reports here. Therefore we catch this exception to avoid letting the test fail because of it. + } + } + + /** + * Ensures that two non-identical, duplicate classes cause an exception to be thrown. + */ + @Test + fun testDifferentClassesWithTheSameNameShouldThrowException() { + assertThatThrownBy { + runGenerator( + "different-duplicate-classes", + EDuplicateClassFileBehavior.FAIL + ) + }.isExactlyInstanceOf(IOException::class.java) + .hasCauseExactlyInstanceOf(IllegalStateException::class.java) + } + + /** + * Ensures that two non-identical, duplicate classes do not cause an exception to be thrown if the ignore-duplicates + * flag is set. + */ + @Test + fun testDifferentClassesWithTheSameNameShouldNotThrowExceptionIfFlagIsSet() { + try { + runGenerator("different-duplicate-classes", EDuplicateClassFileBehavior.IGNORE) + } catch (e: EmptyReportException) { + // An empty report exception is thrown here because we did not configure the correct class IDs to match the + // artificial coverage generated by createDummyDump to the actual class file. Since we are only testing for + // IllegalStateExceptions resulting from duplicated, non-identical class files, and don't care about empty + // reports here. Therefore we catch this exception to avoid letting the test fail because of it. + } + } + + @Test + fun testEmptyCoverageFileThrowsException() { + val testFolderName = "empty-report-handling" + val classId = calculateClassId(testFolderName, "TestClass.class") + assertThatThrownBy { + runGenerator( + testFolderName, + EDuplicateClassFileBehavior.IGNORE, + false, + ClasspathWildcardIncludeFilter("some.package.*", null), + createDummyDump(classId) + ) + }.isExactlyInstanceOf(EmptyReportException::class.java) + } + + @Test + @Throws(IOException::class, EmptyReportException::class) + fun testNonEmptyCoverageFileDoesNotThrowException() { + val testFolderName = "empty-report-handling" + val classId = calculateClassId(testFolderName, "TestClass.class") + runGenerator( + testFolderName, EDuplicateClassFileBehavior.IGNORE, false, + ClasspathWildcardIncludeFilter("*", null), createDummyDump(classId) + ) + } + + /** Ensures that uncovered classes are removed from the report if ignore-uncovered-classes is set. */ + @Test + fun testShrinking() { + val testFolderName = "ignore-uncovered-classes" + val classId = calculateClassId(testFolderName, "TestClass.class") + val stream = ByteArrayOutputStream() + + runGenerator( + testFolderName, EDuplicateClassFileBehavior.FAIL, true, + ClasspathWildcardIncludeFilter("*", null), + createDummyDump(classId) + ).copyStream(stream) + + val xmlString = stream.toString(StandardCharsets.UTF_8.name()) + assertThat(xmlString).contains("TestClass") + assertThat(xmlString).doesNotContain("TestClassTwo") + assertThat(xmlString).doesNotContain("ITestInterface") + } + + /** Ensures that uncovered classes are contained in the report if ignore-uncovered-classes is not set. */ + @Test + fun testNonShrinking() { + val testFolderName = "ignore-uncovered-classes" + val classId = calculateClassId(testFolderName, "TestClass.class") + val stream = ByteArrayOutputStream() + + runGenerator( + testFolderName, EDuplicateClassFileBehavior.FAIL, false, + ClasspathWildcardIncludeFilter("*", null), + createDummyDump(classId) + ).copyStream(stream) + + val xmlString = stream.toString(StandardCharsets.UTF_8.name()) + assertThat(xmlString).contains("TestClassTwo") + } + + @Throws(IOException::class) + private fun calculateClassId(testFolderName: String, classFileName: String): Long { + val classFile = useTestFile(testFolderName + File.separator + classFileName) + return CRC64.classId(Files.readAllBytes(classFile.toPath())) + } + + /** Runs the report generator with default values and without ignoring uncovered classes. */ + @Throws(IOException::class, EmptyReportException::class) + private fun runGenerator( + testDataFolder: String, + duplicateClassFileBehavior: EDuplicateClassFileBehavior, + ignoreUncoveredClasses: Boolean = false, + filter: ClasspathWildcardIncludeFilter = ClasspathWildcardIncludeFilter(null, null), + dump: Dump = createDummyDump() + ): CoverageFile { + val classFileFolder = useTestFile(testDataFolder) + val currentTime = System.currentTimeMillis() + val outputFilePath = "test-coverage-$currentTime.xml" + return JaCoCoXmlReportGenerator( + listOf(classFileFolder), + filter, + duplicateClassFileBehavior, + ignoreUncoveredClasses, + Mockito.mock() + ).convert(dump, Paths.get(outputFilePath).toFile()) + } + + companion object { + /** + * Creates a fake dump with the specified class ID. The class ID can currently be calculated with [org.jacoco.core.internal.data.CRC64.classId]. + * This might change in the future, as it's considered an [implementation detail of JaCoCo](https://www.jacoco.org/jacoco/trunk/doc/classids.html)) + */ + private fun createDummyDump(classId: Long = 123): Dump { + val store = ExecutionDataStore() + store.put(ExecutionData(classId, "TestClass", booleanArrayOf(true, true, true))) + val info = SessionInfo("session-id", 124L, 125L) + return Dump(info, store) + } + } +} diff --git a/report-generator/src/test/kotlin/com/teamscale/report/testwise/jacoco/JaCoCoTestwiseReportGeneratorTest.kt b/report-generator/src/test/kotlin/com/teamscale/report/testwise/jacoco/JaCoCoTestwiseReportGeneratorTest.kt new file mode 100644 index 000000000..792f90d8a --- /dev/null +++ b/report-generator/src/test/kotlin/com/teamscale/report/testwise/jacoco/JaCoCoTestwiseReportGeneratorTest.kt @@ -0,0 +1,69 @@ +package com.teamscale.report.testwise.jacoco + +import com.teamscale.client.TestDetails +import com.teamscale.report.EDuplicateClassFileBehavior +import com.teamscale.report.ReportUtils.getTestwiseCoverageReportAsString +import com.teamscale.report.testwise.model.ETestExecutionResult +import com.teamscale.report.testwise.model.TestExecution +import com.teamscale.report.testwise.model.TestwiseCoverage +import com.teamscale.report.testwise.model.TestwiseCoverageReport +import com.teamscale.report.testwise.model.builder.TestwiseCoverageReportBuilder.Companion.createFrom +import com.teamscale.report.util.ClasspathWildcardIncludeFilter +import com.teamscale.test.TestDataBase +import org.conqat.lib.commons.filesystem.FileSystemUtils +import org.junit.jupiter.api.Test +import org.mockito.Mockito +import org.skyscreamer.jsonassert.JSONAssert +import org.skyscreamer.jsonassert.JSONCompareMode + +/** Tests for the [JaCoCoTestwiseReportGenerator] class. */ +class JaCoCoTestwiseReportGeneratorTest : TestDataBase() { + @Test + fun testSmokeTestTestwiseReportGeneration() { + val report = runReportGenerator("jacoco/cqddl/classes.zip", "jacoco/cqddl/coverage.exec") + val expected = FileSystemUtils.readFileUTF8(useTestFile("jacoco/cqddl/report.json.expected")) + JSONAssert.assertEquals(expected, report, JSONCompareMode.STRICT) + } + + @Test + fun testSampleTestwiseReportGeneration() { + val report = runReportGenerator("jacoco/sample/classes.zip", "jacoco/sample/coverage.exec") + val expected = FileSystemUtils.readFileUTF8(useTestFile("jacoco/sample/report.json.expected")) + JSONAssert.assertEquals(expected, report, JSONCompareMode.STRICT) + } + + @Test + fun defaultPackageIsHandledAsEmptyPath() { + val report = runReportGenerator("jacoco/default-package/classes.zip", "jacoco/default-package/coverage.exec") + val expected = FileSystemUtils.readFileUTF8(useTestFile("jacoco/default-package/report.json.expected")) + JSONAssert.assertEquals(expected, report, JSONCompareMode.STRICT) + } + + @Throws(Exception::class) + private fun runReportGenerator(testDataFolder: String, execFileName: String): String { + val classFileFolder = useTestFile(testDataFolder) + val includeFilter = ClasspathWildcardIncludeFilter(null, null) + val testwiseCoverage = JaCoCoTestwiseReportGenerator( + listOf(classFileFolder), + includeFilter, EDuplicateClassFileBehavior.IGNORE, + Mockito.mock() + ).convert(useTestFile(execFileName)) + return getTestwiseCoverageReportAsString(testwiseCoverage.generateDummyReport()) + } + + companion object { + /** Generates a fake coverage report object that wraps the given [TestwiseCoverage]. */ + fun TestwiseCoverage.generateDummyReport(): TestwiseCoverageReport { + val testDetails = tests.values.map { + TestDetails(it.uniformPath, "/path/to/source", "content") + } + val testExecutions = tests.values.map { + TestExecution( + it.uniformPath, it.uniformPath.length.toLong(), + ETestExecutionResult.PASSED + ) + } + return createFrom(testDetails, tests.values, testExecutions, true) + } + } +} \ No newline at end of file diff --git a/report-generator/src/test/kotlin/com/teamscale/report/testwise/model/builder/FileCoverageBuilderTest.kt b/report-generator/src/test/kotlin/com/teamscale/report/testwise/model/builder/FileCoverageBuilderTest.kt new file mode 100644 index 000000000..fa48fb125 --- /dev/null +++ b/report-generator/src/test/kotlin/com/teamscale/report/testwise/model/builder/FileCoverageBuilderTest.kt @@ -0,0 +1,62 @@ +package com.teamscale.report.testwise.model.builder + +import com.teamscale.report.testwise.model.builder.FileCoverageBuilder.Companion.compactifyToRanges +import com.teamscale.report.util.CompactLines.Companion.compactLinesOf +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatCode +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +/** Tests the [FileCoverageBuilder] class. */ +internal class FileCoverageBuilderTest { + /** Tests the compactification algorithm for line ranges. */ + @Test + fun compactifyRanges() { + val compactLines = compactLinesOf(1, 3, 4, 6, 7, 10) + assertThat(compactifyToRanges(compactLines)) + .hasToString("[1, 3-4, 6-7, 10]") + } + + /** Tests the merge of two [FileCoverageBuilder] objects. */ + @Test + fun mergeDoesMergeRanges() { + FileCoverageBuilder("path", "file").apply { + addLine(1) + addLineRange(3, 4) + addLineRange(7, 10) + + addLineRange(1, 3) + addLineRange(12, 14) + val otherFileCoverage = FileCoverageBuilder("path", "file") + merge(otherFileCoverage) + assertThat(computeCompactifiedRangesAsString()).isEqualTo("1-4,7-10,12-14") + } + } + + /** Tests that two [FileCoverageBuilder] objects from different files throws an exception. */ + @Test + fun mergeDoesNotAllowMergeOfTwoDifferentFiles() { + FileCoverageBuilder("path", "file").apply { + addLine(1) + addLineRange(1, 3) + val otherFileCoverage = FileCoverageBuilder("path", "file2") + assertThatCode { + merge(otherFileCoverage) + }.isInstanceOf(IllegalArgumentException::class.java) + } + } + + @Test + /** Tests the transformation from line ranges into its string representation. */ + fun getRangesAsString() { + FileCoverageBuilder("path", "file").apply { + addLine(1) + addLineRange(3, 4) + addLineRange(6, 10) + assertEquals( + "1,3-4,6-10", + computeCompactifiedRangesAsString() + ) + } + } +} \ No newline at end of file diff --git a/report-generator/src/test/kotlin/com/teamscale/report/util/BashFileSkippingInputStreamTest.kt b/report-generator/src/test/kotlin/com/teamscale/report/util/BashFileSkippingInputStreamTest.kt new file mode 100644 index 000000000..93719bbfd --- /dev/null +++ b/report-generator/src/test/kotlin/com/teamscale/report/util/BashFileSkippingInputStreamTest.kt @@ -0,0 +1,33 @@ +package com.teamscale.report.util + +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import java.io.IOException +import java.util.jar.JarEntry +import java.util.jar.JarInputStream + +internal class BashFileSkippingInputStreamTest { + @Test + @Throws(IOException::class) + fun testBashFileJar() { + val filesInJar = getEntriesFromJarFile("spring-boot-executable-example.jar") + Assertions.assertThat(filesInJar).hasSize(110) + } + + @Test + @Throws(IOException::class) + fun testNormalJar() { + val filesInJar = getEntriesFromJarFile("normal.jar") + Assertions.assertThat(filesInJar).hasSize(284) + } + + @Throws(IOException::class) + private fun getEntriesFromJarFile(resourceName: String): List { + val inputStream = javaClass.getResourceAsStream(resourceName) + val bashFileSkippingInputStream = BashFileSkippingInputStream(inputStream!!) + val jarInputStream = JarInputStream(bashFileSkippingInputStream) + return generateSequence { jarInputStream.nextJarEntry } + .map { it.name } + .toList() + } +} diff --git a/report-generator/src/test/kotlin/com/teamscale/report/util/ClasspathWildcardIncludeFilterTest.kt b/report-generator/src/test/kotlin/com/teamscale/report/util/ClasspathWildcardIncludeFilterTest.kt new file mode 100644 index 000000000..fa7d3b076 --- /dev/null +++ b/report-generator/src/test/kotlin/com/teamscale/report/util/ClasspathWildcardIncludeFilterTest.kt @@ -0,0 +1,36 @@ +package com.teamscale.report.util + +import com.teamscale.report.util.ClasspathWildcardIncludeFilter.Companion.getClassName +import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class ClasspathWildcardIncludeFilterTest { + /** Tests path to class name conversion. */ + @Test + fun testPathToClassNameConversion() { + assertThat(getClassName("file.jar@com/foo/Bar.class")).isEqualTo("com.foo.Bar") + assertThat(getClassName("file.jar@com/foo/Bar\$Goo.class")).isEqualTo("com.foo.Bar.Goo") + assertThat(getClassName("file1.jar@goo/file2.jar@com/foo/Bar.class")).isEqualTo("com.foo.Bar") + assertThat(getClassName("com/foo/Bar.class")).isEqualTo("com.foo.Bar") + assertThat(getClassName("com/foo/Bar")).isEqualTo("com.foo.Bar") + assertThat( + getClassName( + "C:\\client-daily\\client\\plugins\\com.customer.something.client_1.2.3.4.1234566778.jar@com/customer/something/SomeClass.class" + ) + ).isEqualTo("com.customer.something.SomeClass") + } + + + @Test + fun testMatching() { + assertThat( + ClasspathWildcardIncludeFilter(null, "org.junit.*") + .isIncluded("/junit-jupiter-engine-5.1.0.jar@org/junit/jupiter/engine/Constants.class") + ).isFalse() + assertThat( + ClasspathWildcardIncludeFilter(null, "org.junit.*") + .isIncluded("org/junit/platform/commons/util/ModuleUtils\$ModuleReferenceScanner.class") + ).isFalse() + } +} \ No newline at end of file diff --git a/report-generator/src/test/kotlin/com/teamscale/report/util/CompactLinesTest.kt b/report-generator/src/test/kotlin/com/teamscale/report/util/CompactLinesTest.kt new file mode 100644 index 000000000..0da4fb3f4 --- /dev/null +++ b/report-generator/src/test/kotlin/com/teamscale/report/util/CompactLinesTest.kt @@ -0,0 +1,122 @@ +package com.teamscale.report.util + +import com.teamscale.report.util.CompactLines.Companion.compactLinesOf +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.ObjectInputStream +import java.io.ObjectOutputStream + +internal class CompactLinesTest { + @Test + fun emptyList() { + val compactLines = compactLinesOf() + assertThat(compactLines.isEmpty()).isTrue() + } + + @Test + fun testContains() { + val lines = compactLinesOf(5) + assertThat(lines.contains(5)).isTrue() + assertThat(lines.contains(4)).isFalse() + } + + @Test + fun testAddAndRemove() { + val lines = compactLinesOf() + lines.add(10) + assertThat(lines.contains(10)).isTrue() + lines.remove(10) + assertThat(lines.contains(10)).isFalse() + } + + @Test + fun testSize() { + val lines = compactLinesOf(1, 2) + assertThat(lines.size()).isEqualTo(2) + } + + @Test + fun testIsEmpty() { + val lines = compactLinesOf() + assertThat(lines.isEmpty()).isTrue() + lines.add(1) + assertThat(lines.isEmpty()).isFalse() + } + + @Test + fun testMerging() { + val lines1 = compactLinesOf(1, 2) + val lines2 = compactLinesOf() + lines2 merge lines1 + + assertThat(lines2.contains(1)).isTrue() + assertThat(lines2.contains(2)).isTrue() + } + + @Test + fun testRemoveAll() { + val lines1 = compactLinesOf(1, 2) + val lines2 = compactLinesOf(1, 2, 3) + lines2.removeAll(lines1) + + assertThat(lines2.contains(1)).isFalse() + assertThat(lines2.contains(2)).isFalse() + assertThat(lines2.contains(3)).isTrue() + } + + @Test + fun testIntersects() { + val lines1 = compactLinesOf(1, 2) + val lines2 = compactLinesOf(2, 3) + + assertThat(lines1.intersects(lines2)).isTrue() + + lines2.remove(2) + assertThat(lines1.intersects(lines2)).isFalse() + } + + @Test + fun testContainsAny() { + val lines = compactLinesOf(5, 10) + + assertThat(lines.containsAny(3, 4)).isFalse() + assertThat(lines.containsAny(3, 5)).isTrue() + assertThat(lines.containsAny(4, 6)).isTrue() + assertThat(lines.containsAny(10, 15)).isTrue() + assertThat(lines.containsAny(11, 15)).isFalse() + } + + @Test + fun testAddRange() { + val lines = compactLinesOf() + lines.addRange(5, 7) + assertThat(lines).containsExactly(5, 6, 7) + } + + @Test + fun testContainsAllTrue() { + val lines = compactLinesOf(1, 3) + assertThat(lines.containsAll(listOf(1, 2, 3))).isFalse() + assertThat(lines.containsAll(listOf(1, 3))).isTrue() + assertThat(lines.containsAll(compactLinesOf(1, 2, 3))).isFalse() + assertThat(lines.containsAll(compactLinesOf(1, 3))).isTrue() + } + + @Test + fun testSerialization() { + val lines = compactLinesOf(1, 3, 7) + + val bytes = ByteArrayOutputStream().use { outputStream -> + ObjectOutputStream(outputStream).use { it.writeObject(lines) } + outputStream.toByteArray() + } + + val deserializedLines = ByteArrayInputStream(bytes).use { inputStream -> + ObjectInputStream(inputStream).use { it.readObject() as CompactLines } + } + + assertThat(deserializedLines).containsExactly(1, 3, 7) + } +} \ No newline at end of file diff --git a/report-generator/src/test/kotlin/com/teamscale/test/TestDataBase.kt b/report-generator/src/test/kotlin/com/teamscale/test/TestDataBase.kt new file mode 100644 index 000000000..b9737e5a8 --- /dev/null +++ b/report-generator/src/test/kotlin/com/teamscale/test/TestDataBase.kt @@ -0,0 +1,10 @@ +package com.teamscale.test + +import java.io.File + +/** Base class that supports reading test-data files. */ +open class TestDataBase { + /** Read the given test-data file in the context of the current class's package. */ + protected fun useTestFile(fileName: String) = + File(File("test-data", javaClass.getPackage().name), fileName) +} diff --git a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TestImpacted.kt b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TestImpacted.kt index bfa96d081..e6abf890d 100644 --- a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TestImpacted.kt +++ b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TestImpacted.kt @@ -11,6 +11,7 @@ import org.gradle.api.tasks.* import org.gradle.api.tasks.options.Option import org.gradle.api.tasks.testing.Test import org.gradle.api.tasks.testing.junitplatform.JUnitPlatformOptions +import org.gradle.kotlin.dsl.withType import javax.inject.Inject /** Task which runs the impacted tests. */ @@ -203,7 +204,7 @@ abstract class TestImpacted @Inject constructor(objects: ObjectFactory) : Test() return project.configurations .getByName("testRuntimeClasspath") .allDependencies - .withType(ProjectDependency::class.java) + .withType() .map { it.dependencyProject } .flatMap { collectAllDependentJavaProjects(it, seenProjects) } .union(listOf(project)) @@ -219,10 +220,10 @@ abstract class TestImpacted @Inject constructor(objects: ObjectFactory) : Test() if (runImpacted) { assert(endCommit != null) { "When executing only impacted tests a branchName and timestamp must be specified!" } serverConfiguration.validate() - writeEngineProperty("server.url", serverConfiguration.url!!) - writeEngineProperty("server.project", serverConfiguration.project!!) - writeEngineProperty("server.userName", serverConfiguration.userName!!) - writeEngineProperty("server.userAccessToken", serverConfiguration.userAccessToken!!) + writeEngineProperty("server.url", serverConfiguration.url) + writeEngineProperty("server.project", serverConfiguration.project) + writeEngineProperty("server.userName", serverConfiguration.userName) + writeEngineProperty("server.userAccessToken", serverConfiguration.userAccessToken) } writeEngineProperty("partition", report.partition.get()) writeEngineProperty("endCommit", endCommit?.toString()) diff --git a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TestwiseCoverageReportTask.kt b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TestwiseCoverageReportTask.kt index 1601b6495..4cacdeb79 100644 --- a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TestwiseCoverageReportTask.kt +++ b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TestwiseCoverageReportTask.kt @@ -113,7 +113,7 @@ open class TestwiseCoverageReportTask : DefaultTask() { val report = TestwiseCoverageReportBuilder.createFrom( testDetails, - testwiseCoverage.tests, + testwiseCoverage.tests.values, testExecutions, reportConfig.partial ) @@ -149,7 +149,7 @@ fun Logger.wrapInILogger(): ILogger { override fun info(message: String) = logger.info(message) override fun warn(message: String) = logger.warn(message) override fun warn(message: String, throwable: Throwable?) = logger.warn(message, throwable) - override fun error(throwable: Throwable?) = logger.error("", throwable) + override fun error(throwable: Throwable) = logger.error("", throwable) override fun error(message: String, throwable: Throwable?) = logger.error(message, throwable) } } diff --git a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaCoverageConvertMojo.java b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaCoverageConvertMojo.java index 294699b66..81aa2c5a5 100644 --- a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaCoverageConvertMojo.java +++ b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaCoverageConvertMojo.java @@ -89,7 +89,7 @@ public void execute() throws MojoFailureException { classFileDirectories = getClassDirectoriesOrZips(projectBuildDir); findSubprojectReportAndClassDirectories(reportFileDirectories, classFileDirectories); } catch (IOException | AgentOptionParseException e) { - logger.error("Could not create testwise report generator. Aborting."); + logger.error("Could not create testwise report generator. Aborting.", e); throw new MojoFailureException(e); } logger.info("Generating the testwise coverage report"); @@ -129,10 +129,9 @@ private TestInfoFactory createTestInfoFactory(List reportFiles) throws Moj logger.info("Writing report with " + testDetails.size() + " Details/" + testExecutions.size() + " Results"); return new TestInfoFactory(testDetails, testExecutions); } catch (IOException e) { - logger.error("Could not read test details from reports. Aborting."); + logger.error("Could not read test details from reports. Aborting.", e); throw new MojoFailureException(e); } - } private JaCoCoTestwiseReportGenerator createJaCoCoTestwiseReportGenerator(List classFiles) {