diff --git a/CHANGELOG.md b/CHANGELOG.md index f8d214e3a..59602f706 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,13 @@ We use [semantic versioning](http://semver.org/): - PATCH version when you make backwards compatible bug fixes. # Next version +- [breaking] _agent_, _teamscale-maven-plugin_: `includes` and `excludes` only match the class name, no longer parts of the class file's path (e.g. `*target.classes.*`). Nested classes are written with `$` (e.g. `com.company.Outer$Inner`). - [feature] _agent_: Added official support for Java 27 and experimental support for Java 28 (via JaCoCo 0.8.16) +- [fix] _agent_, _teamscale-maven-plugin_: `includes` and `excludes` patterns no longer need a leading `*`. Previously, e.g. `com.company.*` led to an empty testwise coverage report. - [fix] _teamscale-maven-plugin_: `prepare-tia-unit-test` and `prepare-tia-integration-test` no longer require a commit or revision when `runImpacted` is `false`. - [fix] _teamscale-gradle-plugin_, _teamscale-maven-plugin_: Testwise coverage reports no longer drop a class directory that contains an archive (JAR, WAR, EAR, ZIP) that contains a JAR. +- [fix] _agent_: With `duplicates=FAIL`, the error for a duplicate, non-identical class now names the class and how to resolve the problem. +- [fix] _agent_, _teamscale-gradle-plugin_, _teamscale-maven-plugin_: A corrupt class file whose class name cannot be read is skipped with a warning instead of failing the coverage report. # 39.0.0 - [breaking] _agent_: The deprecated Docker image `cqse/teamscale-jacoco-agent` is no longer published. Use `cqse/teamscale-java-profiler` instead, which has replaced it since version 36.5.0. diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/convert/ConvertCommand.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/convert/ConvertCommand.kt index 4dbc3b85c..e204f6284 100644 --- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/convert/ConvertCommand.kt +++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/convert/ConvertCommand.kt @@ -38,10 +38,10 @@ class ConvertCommand : ICommand { */ @Parameter( names = ["--includes"], description = ("" - + "Wildcard include patterns to apply to all found class file locations during JaCoCo's traversal of class files." - + " Note that zip contents are separated from zip files with @ and that you can filter only" - + " class files, not intermediate folders/zips. Use with great care as missing class files" - + " lead to broken coverage files! Turn on debug logging to see which locations are being filtered." + + "Wildcard include patterns for the fully qualified names of the analyzed classes (e.g. com.company.*)." + + " Nested classes are written with \$ (e.g. com.company.Outer\$Inner). Use with great care as missing" + + " class files lead to broken coverage files! Turn on debug logging to see which class files are" + + " being filtered." + " Defaults to no filtering. Excludes overrule includes.") ) var locationIncludeFilters = mutableListOf() @@ -51,10 +51,10 @@ class ConvertCommand : ICommand { */ @Parameter( names = ["--excludes", "-e"], description = ("" - + "Wildcard exclude patterns to apply to all found class file locations during JaCoCo's traversal of class files." - + " Note that zip contents are separated from zip files with @ and that you can filter only" - + " class files, not intermediate folders/zips. Use with great care as missing class files" - + " lead to broken coverage files! Turn on debug logging to see which locations are being filtered." + + "Wildcard exclude patterns for the fully qualified names of the analyzed classes (e.g. com.company.*)." + + " Nested classes are written with \$ (e.g. com.company.Outer\$Inner). Use with great care as missing" + + " class files lead to broken coverage files! Turn on debug logging to see which class files are" + + " being filtered." + " Defaults to no filtering. Excludes overrule includes.") ) var locationExcludeFilters = mutableListOf() diff --git a/agent/src/test/kotlin/com/teamscale/jacoco/agent/options/AgentOptionsTest.kt b/agent/src/test/kotlin/com/teamscale/jacoco/agent/options/AgentOptionsTest.kt index 2aa5545f8..d71e898d3 100644 --- a/agent/src/test/kotlin/com/teamscale/jacoco/agent/options/AgentOptionsTest.kt +++ b/agent/src/test/kotlin/com/teamscale/jacoco/agent/options/AgentOptionsTest.kt @@ -49,24 +49,20 @@ class AgentOptionsTest { @Throws(Exception::class) fun testIncludePatternMatching() { Assertions.assertThat(includeFilter("com.*")).accepts( - "file.jar@com/foo/Bar.class", $$"file.jar@com/foo/Bar$Goo.class", - "file1.jar@goo/file2.jar@com/foo/Bar.class", "com/foo/Bar.class", "com.foo/Bar.class" + "com/foo/Bar", $$"com/foo/Bar$Goo" ) Assertions.assertThat(includeFilter("com.*")).rejects( - "foo/com/Bar.class", "com.class", "file.jar@com.class", - $$"A$com$Bar.class" + "foo/com/Bar", "com", $$"A$com$Bar" ) Assertions.assertThat(includeFilter("*com.*")).accepts( - "file.jar@com/foo/Bar.class", $$"file.jar@com/foo/Bar$Goo.class", - "file1.jar@goo/file2.jar@com/foo/Bar.class", "com/foo/Bar.class", "foo/com/goo/Bar.class", - $$"A$com$Bar.class", "src/com/foo/Bar.class" + "com/foo/Bar", $$"com/foo/Bar$Goo", "foo/com/goo/Bar" ) - Assertions.assertThat(includeFilter("*com.*;*de.*")) - .accepts("file.jar@com/foo/Bar.class", $$"file.jar@de/foo/Bar$Goo.class") - Assertions.assertThat(excludeFilter("*com.*;*de.*")) - .rejects("file.jar@com/foo/Bar.class", $$"file.jar@de/foo/Bar$Goo.class") - Assertions.assertThat(includeFilter("*com.customer.*")).accepts( - "C:\\client-daily\\client\\plugins\\com.customer.something.client_1.2.3.4.1234566778.jar@com/customer/something/SomeClass.class" + Assertions.assertThat(includeFilter("com.*;de.*")) + .accepts("com/foo/Bar", $$"de/foo/Bar$Goo") + Assertions.assertThat(excludeFilter("com.*;de.*")) + .rejects("com/foo/Bar", $$"de/foo/Bar$Goo") + Assertions.assertThat(includeFilter("com.customer.*")).accepts( + "com/customer/something/SomeClass" ) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3b2cc1209..54a4d4bae 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] # When upgrading JaCoCo to a newer version, make sure to -# check the comment in the OpenAnalyzer.java, JaCoCoPreMain.java and CachingInstructionsBuilder.java +# check the comment in the JaCoCoPreMain.java and CachingInstructionsBuilder.java # and update the internal_xxxxxx hash included in the imports in LenientCoverageTransformer.java and JaCoCoPreMain.java. jacoco = "0.8.16" # We need to stay on the 1.3.x release line as 1.4.x requires Java 11 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 deleted file mode 100644 index 983c91d5d..000000000 --- a/report-generator/src/main/java/com/teamscale/report/jacoco/OpenAnalyzer.java +++ /dev/null @@ -1,299 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2009, 2025 Mountainminds GmbH & Co. KG and Contributors - * This program and the accompanying materials are made available under - * the terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * Marc R. Hoffmann - initial API and implementation - * - *******************************************************************************/ - -package com.teamscale.report.jacoco; - -import org.jacoco.core.JaCoCo; -import org.jacoco.core.analysis.Analyzer; -import org.jacoco.core.analysis.ICoverageVisitor; -import org.jacoco.core.data.ExecutionData; -import org.jacoco.core.data.ExecutionDataStore; -import org.jacoco.core.internal.ContentTypeDetector; -import org.jacoco.core.internal.InputStreams; -import org.jacoco.core.internal.Pack200Streams; -import org.jacoco.core.internal.analysis.ClassAnalyzer; -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 org.objectweb.asm.Opcodes; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.StringTokenizer; -import java.util.zip.GZIPInputStream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -/** - * This is a copy of the {@link Analyzer} class from JaCoCo. The only changes are that the following methods are - * protected instead of private: - {@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. - *

- * 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 {@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. - *

- * CAUTION: Do not convert to Kotlin. This class has to stay in Java for future maintenance reasons! - */ -public class OpenAnalyzer { - - private final ExecutionDataStore executionData; - - private final ICoverageVisitor coverageVisitor; - - private final StringPool stringPool; - - /** - * Creates a new analyzer reporting to the given output. - * - * @param executionData execution data - * @param coverageVisitor the output instance that will coverage data for every analyzed class - */ - public OpenAnalyzer(final ExecutionDataStore executionData, - final ICoverageVisitor coverageVisitor) { - this.executionData = executionData; - this.coverageVisitor = coverageVisitor; - this.stringPool = new StringPool(); - } - - /** - * Creates an ASM class visitor for analysis. - * - * @param classId id of the class calculated with {@link CRC64} - * @param className VM name of the class - * @return ASM visitor to write class definition to - */ - private ClassVisitor createAnalyzingVisitor(final long classId, - final String className) { - final ExecutionData data = executionData.get(classId); - final boolean[] probes; - final boolean noMatch; - if (data == null) { - probes = null; - noMatch = executionData.contains(className); - } else { - probes = data.getProbes(); - noMatch = false; - } - final ClassCoverageImpl coverage = new ClassCoverageImpl(className, - classId, noMatch); - final ClassAnalyzer analyzer = new ClassAnalyzer(coverage, probes, - stringPool) { - @Override - public void visitEnd() { - super.visitEnd(); - coverageVisitor.visitCoverage(coverage); - } - }; - return new ClassProbesAdapter(analyzer, false); - } - - /** Analyzes the given class in binary form. */ - protected void analyzeClass(final byte[] source) { - final long classId = CRC64.classId(source); - final ClassReader reader = InstrSupport.classReaderFor(source); - if ((reader.getAccess() & Opcodes.ACC_MODULE) != 0) { - return; - } - if (reader.getClassName().endsWith("/package-info")) { - return; - } - final ClassVisitor visitor = createAnalyzingVisitor(classId, - reader.getClassName()); - reader.accept(visitor, 0); - } - - /** - * Analyzes the class definition from a given in-memory buffer. - * - * @param buffer class definitions - * @param location a location description used for exception messages - * @throws IOException if the class can't be analyzed - */ - public void analyzeClass(final byte[] buffer, final String location) - throws IOException { - try { - analyzeClass(buffer); - } catch (final RuntimeException cause) { - throw analyzerError(location, cause); - } - } - - /** - * Analyzes the class definition from a given input stream. The provided {@link InputStream} is not closed by this - * method. - * - * @param input stream to read class definition from - * @param location a location description used for exception messages - * @throws IOException if the stream can't be read or the class can't be analyzed - */ - public void analyzeClass(final InputStream input, final String location) - throws IOException { - final byte[] buffer; - try { - buffer = InputStreams.readFully(input); - } catch (final IOException e) { - throw analyzerError(location, e); - } - analyzeClass(buffer, location); - } - - /** Creates an {@link IOException} which includes the affected file location and JaCoCo version. */ - protected IOException analyzerError(final String location, - final Exception cause) { - final IOException ex = new IOException( - String.format( - "Could not analyze coverage for '%s' (JaCoCo %s/%s): %s." - + " Verify that the file is a readable .class, JAR, ZIP, GZ, or Pack200 archive" - + " and that it is not corrupt.", - location, JaCoCo.VERSION, JaCoCo.COMMITID_SHORT, - cause.getMessage())); - ex.initCause(cause); - return ex; - } - - /** - * Analyzes all classes found in the given input stream. The input stream may either represent a single class file, - * a ZIP archive, a Pack200 archive or a gzip stream that is searched recursively for class files. All other content - * types are ignored. The provided {@link InputStream} is not closed by this method. - * - * @param input input data - * @param location a location description used for exception messages - * @return number of class files found - * @throws IOException if the stream can't be read or a class can't be analyzed - */ - public int analyzeAll(final InputStream input, final String location) - throws IOException { - final ContentTypeDetector detector; - try { - detector = new ContentTypeDetector(input); - } catch (final IOException e) { - 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; - } - } - - /** - * Analyzes all class files contained in the given file or folder. Class files as well as ZIP files are considered. - * Folders are searched recursively. - * - * @param file file or folder to look for class files - * @return number of class files found - * @throws IOException if the file can't be read or a class can't be analyzed - */ - public int analyzeAll(final File file) throws IOException { - int count = 0; - if (file.isDirectory()) { - for (final File f : file.listFiles()) { - count += analyzeAll(f); - } - } else { - final InputStream in = new FileInputStream(file); - try { - count += analyzeAll(in, file.getPath()); - } finally { - in.close(); - } - } - return count; - } - - /** - * Analyzes all classes from the given class path. Directories containing class files as well as archive files are - * considered. - * - * @param path path definition - * @param basedir optional base directory, if null the current working directory is used as the base - * for relative path entries - * @return number of class files found - * @throws IOException if a file can't be read or a class can't be analyzed - */ - public int analyzeAll(final String path, final File basedir) - throws IOException { - int count = 0; - final StringTokenizer st = new StringTokenizer(path, - File.pathSeparator); - while (st.hasMoreTokens()) { - count += analyzeAll(new File(basedir, st.nextToken())); - } - return count; - } - - private int analyzeZip(final InputStream input, final String location) - throws IOException { - final ZipInputStream zip = new ZipInputStream(input); - ZipEntry entry; - int count = 0; - while ((entry = nextEntry(zip, location)) != null) { - count += analyzeAll(zip, location + "@" + entry.getName()); - } - return count; - } - - private ZipEntry nextEntry(final ZipInputStream input, - final String location) throws IOException { - try { - return input.getNextEntry(); - } catch (final IOException e) { - throw analyzerError(location, e); - } catch (final IllegalArgumentException e) { - // might be thrown in JDK versions below 23 - see - // https://bugs.openjdk.org/browse/JDK-8321156 - // https://github.com/openjdk/jdk/commit/20c71ceacdcb791f5b70cda456bdc47bdd9acf6c - throw analyzerError(location, e); - } - } - - private int analyzeGzip(final InputStream input, final String location) - throws IOException { - GZIPInputStream gzipInputStream; - try { - gzipInputStream = new GZIPInputStream(input); - } catch (final IOException e) { - throw analyzerError(location, e); - } - return analyzeAll(gzipInputStream, location); - } - - private int analyzePack200(final InputStream input, final String location) - throws IOException { - InputStream unpackedInput; - try { - unpackedInput = Pack200Streams.unpack(input); - } catch (final IOException e) { - throw analyzerError(location, e); - } - return analyzeAll(unpackedInput, location); - } - -} 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 index 0d968b445..bf2527fb4 100644 --- a/report-generator/src/main/kotlin/com/teamscale/report/jacoco/FilteringAnalyzer.kt +++ b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/FilteringAnalyzer.kt @@ -3,15 +3,18 @@ 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.JaCoCo +import org.jacoco.core.analysis.Analyzer import org.jacoco.core.analysis.ICoverageVisitor import org.jacoco.core.data.ExecutionDataStore +import org.jacoco.core.internal.instr.InstrSupport +import org.objectweb.asm.ClassReader +import org.objectweb.asm.Opcodes 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. + * [Analyzer] that filters the analyzed class files by their class name. * * @param executionData The execution data store. * @param coverageVisitor The coverage visitor. @@ -23,26 +26,36 @@ open class FilteringAnalyzer( coverageVisitor: ICoverageVisitor?, private val locationIncludeFilter: ClasspathWildcardIncludeFilter, private val logger: ILogger -) : OpenAnalyzer(executionData, coverageVisitor) { +) : Analyzer(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) + try { + if (location.endsWith(".jar")) { + return analyzeJar(input, location) + } + return super.analyzeAll(input, location) + } catch (e: AnalyzerException) { + throw e + } catch (e: IOException) { + // JaCoCo's error for this location (e.g. a corrupt ZIP) lacks the message of its cause and a hint + throw analyzerError(location, e.cause as? Exception ?: e) } - return super.analyzeAll(input, location) } @Throws(IOException::class) override fun analyzeClass(buffer: ByteArray, location: String) { try { - analyzeClass(buffer) + val (reader, className) = readHeader(buffer, location) ?: return + if (isIncluded(className, location)) { + analyzeIncludedClass(buffer, reader, location) + } } catch (cause: RuntimeException) { if (cause.isUnsupportedClassFile()) { + val className = readClassNameOfUnsupportedClassFile(buffer) + if (className != null && !isIncluded(className, location)) { + return + } logger.error( "Unsupported class file in '$location': ${cause.message}." + " This is usually a class file compiled for a Java version newer than the profiler supports" + @@ -56,32 +69,93 @@ open class FilteringAnalyzer( } /** - * 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. + * Creates a reader for the class file and reads its class name. This only reads the constant pool, which is cheap + * compared to the analysis of the whole class. A corrupt class file is logged and skipped by returning `null` instead + * of failing the analysis: without its class name, we cannot tell whether it is excluded. */ - private fun RuntimeException.isUnsupportedClassFile() = - this is IllegalArgumentException && message?.startsWith("Unsupported") == true + private fun readHeader(buffer: ByteArray, location: String): Pair? { + try { + val reader = InstrSupport.classReaderFor(buffer) + return reader to reader.className + } catch (cause: RuntimeException) { + if (cause.isUnsupportedClassFile()) { + throw cause + } + logger.warn( + "Skipping class file '$location', whose class name cannot be read ($cause). The file is probably corrupt.", + cause + ) + return null + } + } /** - * Copied from [org.jacoco.core.analysis.Analyzer.analyzeZip] renamed to analyzeJar - * and added wrapping [BashFileSkippingInputStream]. + * Reads the class name of a class file whose version ASM does not support from a copy with a supported version, so + * that such a class file can still be excluded. Returns `null` if the class name cannot be read anyway, e.g. + * because the class file uses constant pool entries that ASM does not know. */ - @Throws(IOException::class) - protected open fun analyzeJar(input: InputStream, location: String): Int { - val zip = ZipInputStream(BashFileSkippingInputStream(input)) - return generateSequence { zip.nextEntry(location) } - .map { entry -> analyzeAll(zip, "$location@${entry.name}") } - .sum() + private fun readClassNameOfUnsupportedClassFile(buffer: ByteArray): String? { + val supportedCopy = buffer.copyOf() + InstrSupport.setMajorVersion(Opcodes.V1_8, supportedCopy) + return try { + ClassReader(supportedCopy).className + } catch (_: RuntimeException) { + null + } } - /** Copied from [org.jacoco.core.analysis.Analyzer.nextEntry]. */ - @Throws(IOException::class) - private fun ZipInputStream.nextEntry(location: String): ZipEntry? { + /** Tests if the class with the given JVM internal name passes the filter and logs if it does not. */ + private fun isIncluded(className: String, location: String): Boolean { + if (locationIncludeFilter.isIncluded(className)) { + return true + } + logger.debug("Excluding class file $location ($className)") + return false + } + + /** + * Analyzes a class that passed the filter. [reader] is the filter's reader of [buffer], which JaCoCo's analysis + * cannot take, so only subclasses reuse it. Errors are thrown as [RuntimeException], so that [analyzeClass] reports + * them uniformly. + */ + protected open fun analyzeIncludedClass(buffer: ByteArray, reader: ClassReader, location: String) { try { - return nextEntry + super.analyzeClass(buffer, location) } catch (e: IOException) { - throw analyzerError(location, e) + // JaCoCo wraps the RuntimeException of the analysis into an IOException + throw e.cause as? RuntimeException ?: e } } + + /** Creates an [IOException] which includes the affected file location and JaCoCo version. */ + protected fun analyzerError(location: String, cause: Exception): IOException = AnalyzerException( + "Could not analyze coverage for '$location' (JaCoCo ${JaCoCo.VERSION}/${JaCoCo.COMMITID_SHORT}): " + + "${cause.message}. Verify that the file is a readable .class, JAR, ZIP, GZ, or Pack200 archive" + + " and that it is not corrupt.", + cause + ) + + /** + * Error of [analyzerError], which [analyzeAll] passes on unchanged to the locations that contain its location. + * + * @param message The exception message. + * @param cause The error that occurred during the analysis. + */ + private class AnalyzerException(message: String, cause: Exception) : IOException(message, 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 + + /** + * Analyzes a JAR, skipping a shell script in front of it (e.g. Spring Boot executable JARs) that would otherwise + * keep JaCoCo from detecting the ZIP format. + */ + @Throws(IOException::class) + protected open fun analyzeJar(input: InputStream, location: String) = + super.analyzeAll(BashFileSkippingInputStream(input), location) } diff --git a/report-generator/src/main/kotlin/com/teamscale/report/jacoco/JaCoCoBasedReportGenerator.kt b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/JaCoCoBasedReportGenerator.kt index 5ce800429..a6fee2ea0 100644 --- a/report-generator/src/main/kotlin/com/teamscale/report/jacoco/JaCoCoBasedReportGenerator.kt +++ b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/JaCoCoBasedReportGenerator.kt @@ -155,10 +155,10 @@ abstract class JaCoCoBasedReportGenerator( return } - EDuplicateClassFileBehavior.FAIL -> error { + EDuplicateClassFileBehavior.FAIL -> error( "Duplicate non-identical class '${coverage.name}' encountered with 'duplicates=FAIL'." + " Either resolve the duplicate in your application or set 'duplicates=WARN'." - } + ) } } } 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 index 4ea7496dd..bab6dbe7d 100644 --- 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 @@ -8,7 +8,7 @@ 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 java.io.File import java.io.IOException import java.io.InputStream @@ -17,8 +17,8 @@ import java.nio.file.Files /** * 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. + * For every included class that gets found, [analyzeIncludedClass] 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, @@ -28,15 +28,14 @@ class AnalyzerCache( private val stringPool = StringPool() /** - * Analyzes the given class. Instead of the original implementation in [Analyzer.analyzeClass] we + * Analyzes the given class. Instead of the original implementation in [FilteringAnalyzer.analyzeIncludedClass] 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) + override fun analyzeIncludedClass(buffer: ByteArray, reader: ClassReader, location: String) { + val classId = CRC64.classId(buffer) 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 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 index a6dcca6ef..07b2b6fe0 100644 --- 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 @@ -74,9 +74,8 @@ class ProbesCache( ): FileCoverageBuilder? { val classId = executionData.id if (!containsClassId(classId)) { - val fullyQualifiedClassName = JavaNames().getQualifiedClassName(executionData.name) - if (locationIncludeFilter.isIncluded("$fullyQualifiedClassName.class")) { - classNotFoundLogger.log(fullyQualifiedClassName) + if (locationIncludeFilter.isIncluded(executionData.name)) { + classNotFoundLogger.log(JavaNames().getQualifiedClassName(executionData.name)) } return null } 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 index ffafb1225..e26b11f23 100644 --- a/report-generator/src/main/kotlin/com/teamscale/report/util/ClasspathWildcardIncludeFilter.kt +++ b/report-generator/src/main/kotlin/com/teamscale/report/util/ClasspathWildcardIncludeFilter.kt @@ -1,16 +1,11 @@ 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.* -import java.util.concurrent.ConcurrentHashMap /*** - * 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" + * Tests class names against wildcard include and exclude patterns, the same way JaCoCo's agent does. + * E.g. "com/example/Test" matches "com.example.*" or "com.example.Test". */ open class ClasspathWildcardIncludeFilter( locationIncludeFilters: String?, @@ -44,43 +39,14 @@ open class ClasspathWildcardIncludeFilter( } /** - * Tests if the given file path (e.g. "/some/file/path/test.jar@my/package/Test.class" or "org/mypackage/MyClass" + * Tests if the class with the given JVM internal name (e.g. "com/example/Outer$Inner") is included. Like JaCoCo, + * only slashes are replaced by dots, so nested classes keep their "$". */ - fun isIncluded(path: String): Boolean { - val className = getClassName(path) - if (locationIncludeFilters != null && locationIncludeFilters?.matches(className) == false) { + fun isIncluded(internalClassName: String): Boolean { + val dottedClassName = internalClassName.replace('/', '.') + if (locationIncludeFilters != null && locationIncludeFilters?.matches(dottedClassName) == false) { return false } - return locationExcludeFilters == null || locationExcludeFilters?.matches(className) == false - } - - - companion object { - private val AT_REGEX = "@".toRegex() - private val JAVA_NAMES = JavaNames() - - private val classNameCache = ConcurrentHashMap(1024) - - /** - * 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". - */ - fun getClassName(path: String): String = - classNameCache.computeIfAbsent(path) { computeClassName(it) } - - private fun computeClassName(path: String): String { - val parts = FileSystemUtils.normalizeSeparators(path) - .split(AT_REGEX).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 JAVA_NAMES.getQualifiedClassName(pathInsideJar) - } + return locationExcludeFilters == null || locationExcludeFilters?.matches(dottedClassName) == false } } diff --git a/report-generator/src/test/kotlin/com/teamscale/report/jacoco/FilteringAnalyzerTest.kt b/report-generator/src/test/kotlin/com/teamscale/report/jacoco/FilteringAnalyzerTest.kt new file mode 100644 index 000000000..1a7ac225c --- /dev/null +++ b/report-generator/src/test/kotlin/com/teamscale/report/jacoco/FilteringAnalyzerTest.kt @@ -0,0 +1,156 @@ +package com.teamscale.report.jacoco + +import com.teamscale.report.util.ClasspathWildcardIncludeFilter +import com.teamscale.report.util.ILogger +import com.teamscale.test.zip +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.jacoco.core.data.ExecutionDataStore +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.mockito.Mockito +import java.io.EOFException +import java.io.File +import java.io.IOException + +/** Tests that [FilteringAnalyzer] filters class files by their class name, independent of their location. */ +class FilteringAnalyzerTest { + + @TempDir + private lateinit var tempDir: File + + private val classBytes = resourceBytes("FilteringAnalyzerTest.class") + + /** + * The location of a class file must not influence whether it is included: class directories may contain further + * directories above the package structure, class files need not be in a directory matching their package (e.g. + * in Kotlin projects) and archives may contain classes below e.g. `BOOT-INF/classes`. + */ + @ParameterizedTest + @CsvSource( + "com.teamscale.report.*, , true", + "*com.teamscale.report.*, , true", + "com.teamscale.report.jacoco.FilteringAnalyzerTest, , true", + "org.other.*, , false", + ", com.teamscale.*, false", + "com.teamscale.*, com.teamscale.report.jacoco.*, false", + ) + fun filtersByClassNameIndependentOfLocation(includes: String?, excludes: String?, expectedIncluded: Boolean) { + val locations = mapOf( + "build/classes/main/com/teamscale/report/jacoco/FilteringAnalyzerTest.class" to classBytes, + "flat/FilteringAnalyzerTest.class" to classBytes, + "app.jar" to zip("BOOT-INF/classes/com/teamscale/report/jacoco/FilteringAnalyzerTest.class" to classBytes), + "app.war" to zip("WEB-INF/classes/com/teamscale/report/jacoco/FilteringAnalyzerTest.class" to classBytes), + ) + locations.forEach { (path, content) -> tempDir.resolve(path).write(content) } + val filter = ClasspathWildcardIncludeFilter(includes, excludes) + + val includedLocations = locations.keys.filter { analyze(filter, file = tempDir.resolve(it)).isNotEmpty() } + + assertThat(includedLocations).isEqualTo(if (expectedIncluded) locations.keys.toList() else emptyList()) + } + + /** Like JaCoCo, nested classes are matched with their `$`. */ + @Test + fun matchesNestedClassesWithDollar() { + tempDir.resolve("Outer.class").write(classBytes) + tempDir.resolve("Nested.class").write(resourceBytes("FilteringAnalyzerTest\$Nested.class")) + + assertThat(analyze(ClasspathWildcardIncludeFilter("com.teamscale.report.jacoco.FilteringAnalyzerTest$*", null))) + .containsExactly("com/teamscale/report/jacoco/FilteringAnalyzerTest\$Nested") + } + + /** A JAR with a shell script in front, e.g. a Spring Boot executable JAR, must be analyzed like a plain JAR. */ + @Test + fun analyzesExecutableJars() { + tempDir.resolve("app.jar").write(resourceBytes("/com/teamscale/report/util/spring-boot-executable-example.jar")) + + assertThat(analyze(ClasspathWildcardIncludeFilter("org.springframework.boot.loader.jar.*", null))) + .contains("org/springframework/boot/loader/jar/Bytes") + .allMatch { it.startsWith("org/springframework/boot/loader/jar/") } + } + + /** A class file too new for JaCoCo must be logged and skipped instead of failing the whole report. */ + @Test + fun skipsUnsupportedClassFiles() { + tempDir.resolve("Future.class").write(withUnsupportedVersion(classBytes)) + tempDir.resolve("Supported.class").write(classBytes) + val logger = Mockito.mock() + + assertThat(analyze(ClasspathWildcardIncludeFilter(null, null), logger)) + .containsExactly("com/teamscale/report/jacoco/FilteringAnalyzerTest") + Mockito.verify(logger).error(Mockito.contains("Unsupported class file"), Mockito.any(Throwable::class.java)) + } + + /** A class file too new for JaCoCo must be skipped without an error if it is excluded, as the error recommends. */ + @Test + fun skipsExcludedUnsupportedClassFilesWithoutError() { + tempDir.resolve("Future.class").write(withUnsupportedVersion(classBytes)) + val logger = Mockito.mock() + + assertThat(analyze(ClasspathWildcardIncludeFilter(null, "com.teamscale.*"), logger)).isEmpty() + Mockito.verify(logger).debug(Mockito.contains("Excluding class file")) + Mockito.verifyNoMoreInteractions(logger) + } + + /** + * A class file whose class name cannot be read must be logged and skipped instead of failing the whole report, + * since it may be excluded. + */ + @Test + fun skipsCorruptClassFiles() { + // Cuts the class file in its constant pool, which holds the class name + tempDir.resolve("Corrupt.class").write(classBytes.copyOf(40)) + tempDir.resolve("Supported.class").write(classBytes) + val logger = Mockito.mock() + + assertThat(analyze(ClasspathWildcardIncludeFilter(null, null), logger)) + .containsExactly("com/teamscale/report/jacoco/FilteringAnalyzerTest") + Mockito.verify(logger).warn(Mockito.contains("Corrupt.class"), Mockito.any(Throwable::class.java)) + } + + /** An archive that JaCoCo cannot read must be reported with its location, the cause and a hint. */ + @Test + fun reportsUnreadableArchives() { + val jar = zip("com/teamscale/report/jacoco/FilteringAnalyzerTest.class" to classBytes) + // Cuts the JAR in the middle of the class file's data + tempDir.resolve("app.jar").write(jar.copyOf(jar.size / 2)) + + assertThatThrownBy { analyze(ClasspathWildcardIncludeFilter(null, null)) } + .isInstanceOf(IOException::class.java) + .hasMessageContaining("app.jar@com/teamscale/report/jacoco/FilteringAnalyzerTest.class") + .hasMessageContaining("Verify that the file is a readable") + .hasCauseInstanceOf(EOFException::class.java) + } + + private fun analyze( + filter: ClasspathWildcardIncludeFilter, + logger: ILogger = Mockito.mock(), + file: File = tempDir + ): List { + val analyzedClasses = mutableListOf() + FilteringAnalyzer(ExecutionDataStore(), { analyzedClasses.add(it.name) }, filter, logger).analyzeAll(file) + return analyzedClasses + } + + private fun File.write(content: ByteArray) { + parentFile.mkdirs() + writeBytes(content) + } + + private fun resourceBytes(name: String) = javaClass.getResourceAsStream(name)!!.use { it.readBytes() } + + /** Returns a copy of the given class file with a major version that is too new for JaCoCo. */ + private fun withUnsupportedVersion(classBytes: ByteArray): ByteArray { + val unsupportedClassBytes = classBytes.copyOf() + // Bytes 6 and 7 hold the class file's major version + unsupportedClassBytes[6] = 0x7F + unsupportedClassBytes[7] = 0x7F + return unsupportedClassBytes + } + + /** A nested class to test the matching of nested class names. */ + private class Nested +} 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 index 65bd154a0..0ce2ca333 100644 --- a/report-generator/src/test/kotlin/com/teamscale/report/jacoco/JaCoCoXmlReportGeneratorTest.kt +++ b/report-generator/src/test/kotlin/com/teamscale/report/jacoco/JaCoCoXmlReportGeneratorTest.kt @@ -57,8 +57,9 @@ class JaCoCoXmlReportGeneratorTest : TestDataBase() { "different-duplicate-classes", EDuplicateClassFileBehavior.FAIL ) - }.isExactlyInstanceOf(IOException::class.java) + }.isInstanceOf(IOException::class.java) .hasCauseExactlyInstanceOf(IllegalStateException::class.java) + .hasMessageContaining("Duplicate non-identical class") } /** @@ -105,8 +106,9 @@ class JaCoCoXmlReportGeneratorTest : TestDataBase() { "different-duplicate-classes", EDuplicateClassFileBehavior.FAIL ) - }.isExactlyInstanceOf(IOException::class.java) + }.isInstanceOf(IOException::class.java) .hasCauseExactlyInstanceOf(IllegalStateException::class.java) + .hasMessageContaining("Duplicate non-identical class") } /** diff --git a/report-generator/src/test/kotlin/com/teamscale/report/testwise/jacoco/cache/AnalyzerCacheTest.kt b/report-generator/src/test/kotlin/com/teamscale/report/testwise/jacoco/cache/AnalyzerCacheTest.kt index 556794a6c..64038f0a0 100644 --- a/report-generator/src/test/kotlin/com/teamscale/report/testwise/jacoco/cache/AnalyzerCacheTest.kt +++ b/report-generator/src/test/kotlin/com/teamscale/report/testwise/jacoco/cache/AnalyzerCacheTest.kt @@ -3,6 +3,7 @@ package com.teamscale.report.testwise.jacoco.cache import com.teamscale.report.EDuplicateClassFileBehavior import com.teamscale.report.util.ClasspathWildcardIncludeFilter import com.teamscale.report.util.ILogger +import com.teamscale.test.zip import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.jacoco.core.internal.data.CRC64 @@ -11,11 +12,8 @@ import org.junit.jupiter.api.io.TempDir import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.CsvSource import org.mockito.Mockito -import java.io.ByteArrayOutputStream import java.io.File import java.io.IOException -import java.util.zip.ZipEntry -import java.util.zip.ZipOutputStream /** Tests for the [AnalyzerCache] class. */ class AnalyzerCacheTest { @@ -44,6 +42,17 @@ class AnalyzerCacheTest { assertThat(probesCache.containsClassId(CRC64.classId(classBytes))).isTrue() } + /** Excluded classes must not be added to the cache. */ + @Test + fun skipsExcludedClasses() { + tempDir.resolve("AnalyzerCacheTest.class").writeBytes(classBytes) + val excludingAnalyzer = AnalyzerCache(probesCache, ClasspathWildcardIncludeFilter(null, "com.teamscale.*"), logger) + + excludingAnalyzer.analyzeAll(tempDir) + + assertThat(probesCache.containsClassId(CRC64.classId(classBytes))).isFalse() + } + /** A nested JAR that cannot be read must be reported with its location, so that users can find it. */ @Test fun reportsLocationOfUnreadableNestedJar() { @@ -55,16 +64,4 @@ class AnalyzerCacheTest { .isInstanceOf(IOException::class.java) .hasMessageContaining("outer.jar@lib/inner.jar") } - - private fun zip(vararg entries: Pair): ByteArray { - val output = ByteArrayOutputStream() - ZipOutputStream(output).use { zip -> - entries.forEach { (name, content) -> - zip.putNextEntry(ZipEntry(name)) - zip.write(content) - zip.closeEntry() - } - } - return output.toByteArray() - } } diff --git a/report-generator/src/test/kotlin/com/teamscale/report/testwise/jacoco/cache/ProbesCacheTest.kt b/report-generator/src/test/kotlin/com/teamscale/report/testwise/jacoco/cache/ProbesCacheTest.kt new file mode 100644 index 000000000..cc6d70911 --- /dev/null +++ b/report-generator/src/test/kotlin/com/teamscale/report/testwise/jacoco/cache/ProbesCacheTest.kt @@ -0,0 +1,29 @@ +package com.teamscale.report.testwise.jacoco.cache + +import com.teamscale.report.EDuplicateClassFileBehavior +import com.teamscale.report.util.ClasspathWildcardIncludeFilter +import com.teamscale.report.util.ILogger +import org.assertj.core.api.Assertions.assertThat +import org.jacoco.core.data.ExecutionData +import org.junit.jupiter.api.Test +import org.mockito.Mockito + +/** Tests for the [ProbesCache] class. */ +class ProbesCacheTest { + + private val logger = Mockito.mock() + private val probesCache = ProbesCache(logger, EDuplicateClassFileBehavior.IGNORE) + + /** Coverage of classes that were not analyzed must only be reported as missing if the classes are included. */ + @Test + fun reportsMissingClassesOnlyIfIncluded() { + val filter = ClasspathWildcardIncludeFilter("com.company.*", null) + + assertThat(probesCache.getCoverage(ExecutionData(1, "com/company/Included", 1), filter)).isNull() + assertThat(probesCache.getCoverage(ExecutionData(2, "org/other/Excluded", 1), filter)).isNull() + probesCache.flushLogger() + + Mockito.verify(logger).warn(" - com.company.Included") + Mockito.verify(logger, Mockito.never()).warn(Mockito.contains("org.other.Excluded")) + } +} 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 index fa7d3b076..b16c0cee1 100644 --- a/report-generator/src/test/kotlin/com/teamscale/report/util/ClasspathWildcardIncludeFilterTest.kt +++ b/report-generator/src/test/kotlin/com/teamscale/report/util/ClasspathWildcardIncludeFilterTest.kt @@ -1,36 +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 +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource 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() + /** Tests the include and exclude patterns against JVM internal class names. */ + @ParameterizedTest + @CsvSource( + // Patterns must match the class name with and without leading `*` + "com.company.*, , com/company/Foo, true", + "com.company.*, , com/company/sub/Foo, true", + "com.company.*, , org/company/Foo, false", + "com.company.*, , org/com/company/Foo, false", + "*com.company.*, , com/company/Foo, true", + "*com.company.*, , org/com/company/Foo, true", + // Excludes overrule includes + ", org.junit.*, org/junit/jupiter/engine/Constants, false", + ", org.junit.*, com/company/Foo, true", + "com.company.*, com.company.generated.*, com/company/Foo, true", + "com.company.*, com.company.generated.*, com/company/generated/Foo, false", + // Like JaCoCo, nested classes keep their `$` + "com.company.Outer$*, , com/company/Outer\$Inner, true", + "com.company.Outer.Inner, , com/company/Outer\$Inner, false", + // Multiple patterns are separated by colons + "com.*:de.*, , com/foo/Bar, true", + "com.*:de.*, , de/foo/Bar\$Goo, true", + "com.*:de.*, , org/foo/Bar, false", + ) + fun testIsIncluded(includes: String?, excludes: String?, internalClassName: String, expectedIncluded: Boolean) { + assertThat(ClasspathWildcardIncludeFilter(includes, excludes).isIncluded(internalClassName)) + .isEqualTo(expectedIncluded) } -} \ No newline at end of file +} diff --git a/report-generator/src/test/kotlin/com/teamscale/test/ZipTestUtils.kt b/report-generator/src/test/kotlin/com/teamscale/test/ZipTestUtils.kt new file mode 100644 index 000000000..6cabd3d5d --- /dev/null +++ b/report-generator/src/test/kotlin/com/teamscale/test/ZipTestUtils.kt @@ -0,0 +1,18 @@ +package com.teamscale.test + +import java.io.ByteArrayOutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +/** Creates a ZIP archive (e.g. a JAR or WAR) with the given entries, each given by its name and content. */ +fun zip(vararg entries: Pair): ByteArray { + val output = ByteArrayOutputStream() + ZipOutputStream(output).use { zip -> + entries.forEach { (name, content) -> + zip.putNextEntry(ZipEntry(name)) + zip.write(content) + zip.closeEntry() + } + } + return output.toByteArray() +} diff --git a/sample-app/java-profiler.properties b/sample-app/java-profiler.properties index 37729c4ba..b926d314d 100644 --- a/sample-app/java-profiler.properties +++ b/sample-app/java-profiler.properties @@ -19,7 +19,7 @@ debug=true # Local configuration -includes=*com.example.* +includes=com.example.* # Uncomment and complete the following to upload coverage to a local Teamscale instance. #teamscale-project= #teamscale-partition=Agent Debugging diff --git a/system-tests/tia-maven/maven-dump-local-project/pom.xml b/system-tests/tia-maven/maven-dump-local-project/pom.xml index 55b4be13d..058433bed 100644 --- a/system-tests/tia-maven/maven-dump-local-project/pom.xml +++ b/system-tests/tia-maven/maven-dump-local-project/pom.xml @@ -51,7 +51,7 @@ true false - *foo.* + foo.* diff --git a/system-tests/tia-maven/maven-project/pom.xml b/system-tests/tia-maven/maven-project/pom.xml index 96cb549e7..64b824bf2 100644 --- a/system-tests/tia-maven/maven-project/pom.xml +++ b/system-tests/tia-maven/maven-project/pom.xml @@ -54,7 +54,7 @@ ${baselineCommit} myRepoId - *example.* + org.example.* diff --git a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/AgentConfiguration.kt b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/AgentConfiguration.kt index aa1102ac1..590576616 100644 --- a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/AgentConfiguration.kt +++ b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/AgentConfiguration.kt @@ -1,6 +1,5 @@ package com.teamscale.config -import com.teamscale.report.util.ClasspathWildcardIncludeFilter import com.teamscale.utils.ArgumentAppender import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl @@ -64,14 +63,6 @@ abstract class AgentConfiguration @Inject constructor( remoteAgent = TeamscaleAgent(url.toHttpUrl()) } - /** Returns a filter predicate that respects the configured wildcard include and exclude patterns. */ - fun getPredicate(): ClasspathWildcardIncludeFilter { - return ClasspathWildcardIncludeFilter( - jacocoExtension.includes?.joinToString(":") { "*$it".replace('/', '.') }, - jacocoExtension.excludes?.joinToString(":") { "*$it".replace('/', '.') } - ) - } - inner class TeamscaleAgent(val url: HttpUrl) { /** Builds the jvm argument to start the impacted test executor. */