diff --git a/CHANGELOG.md b/CHANGELOG.md index 511f8222e..25a09cb87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ We use [semantic versioning](http://semver.org/): - PATCH version when you make backwards compatible bug fixes. # Next version +- [breaking feature] _agent_: Coverage is now reported in the Teamscale Compact Coverage format by default in NORMAL mode, which is significantly smaller and faster to upload. Set `report-format=JACOCO` in the agent options to keep the previous JaCoCo XML behavior. The `convert` CLI now picks the format from the output file extension (`.xml` for JaCoCo XML, `.json` for Teamscale Compact Coverage). - [breaking] _report-generator_: `RevisionInfo` is now a sealed class with polymorphic Jackson serialization (`@JsonTypeInfo` / `@JsonSubTypes`). The JSON representation now uses "COMMIT" and "REVISION" as type discriminator values instead of the previous `ERevisionType` enum names. - [feature] _agent_: Added official support for Java 26 and experimental support for Java 27 (via JaCoCo 0.8.15) diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/Agent.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/Agent.kt index a3f33ab62..65aa5d082 100644 --- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/Agent.kt +++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/Agent.kt @@ -10,7 +10,6 @@ import com.teamscale.jacoco.agent.upload.teamscale.TeamscaleUploader import com.teamscale.jacoco.agent.util.AgentUtils import com.teamscale.report.jacoco.CoverageFile import com.teamscale.report.jacoco.EmptyReportException -import com.teamscale.report.jacoco.JaCoCoXmlReportGenerator import com.teamscale.report.jacoco.dump.Dump import org.glassfish.jersey.server.ResourceConfig import org.glassfish.jersey.server.ServerProperties @@ -149,22 +148,17 @@ class Agent(options: AgentOptions, instrumentation: Instrumentation?) : AgentBas return } - val generator = JaCoCoXmlReportGenerator( - options.classDirectoriesOrZips, - options.locationIncludeFilter, - options.duplicateClassFileBehavior, - options.ignoreUncoveredClasses, - LoggingUtils.wrap(logger) - ) + val format = options.normalModeReportFormat + val generator = options.createReportGenerator(LoggingUtils.wrap(logger)) try { - benchmark("Generating the XML report") { - val outputFile = options.createNewFileInOutputDirectory("jacoco", "xml") + benchmark("Generating the coverage report") { + val outputFile = options.createNewFileInOutputDirectory(format.fileNamePrefix, format.fileExtension) val coverageFile = generator.convertSingleDumpToReport(dump, outputFile) uploader.upload(coverageFile) } } catch (e: IOException) { - logger.error("Converting binary dump to XML failed", e) + logger.error("Converting binary dump to coverage report failed", e) } catch (e: EmptyReportException) { logger.error("No coverage was collected. ${e.message}", e) } 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 c25bc62fd..4dbc3b85c 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 @@ -18,8 +18,10 @@ import java.io.IOException * Encapsulates all command line options for the convert command for parsing with [JCommander]. */ @Parameters( - commandNames = ["convert"], commandDescription = "Converts a binary .exec coverage file to XML. " + - "Note that the XML report will only contain source file coverage information, but no class coverage." + commandNames = ["convert"], commandDescription = "Converts a binary .exec coverage file to a coverage report. " + + "The output format is selected by the extension of --out: .xml for JaCoCo XML, .json for Teamscale " + + "Compact Coverage. Note that the report will only contain source file coverage information, but no " + + "class coverage." ) class ConvertCommand : ICommand { /** The directories and/or zips that contain all class files being profiled. */ @@ -65,11 +67,13 @@ class ConvertCommand : ICommand { ) var inputFiles = mutableListOf() - /** The directory to write the XML traces to. */ + /** The output file. The extension selects the format: .xml = JaCoCo XML, .json = Teamscale Compact Coverage. */ @JvmField @Parameter( names = ["--out", "-o"], required = true, description = ("" - + "The file to write the generated XML report to.") + + "The file to write the generated coverage report to. The file extension selects the format: " + + ".xml for JaCoCo XML, .json for Teamscale Compact Coverage. When --testwise-coverage is set, " + + "testwise JSON is produced regardless of the extension.") ) var outputFile = "" @@ -86,7 +90,8 @@ class ConvertCommand : ICommand { @Parameter( names = ["--ignore-uncovered-classes"], required = false, arity = 1, description = ("" + "Whether to ignore uncovered classes." - + " These classes will not be part of the XML report at all, making it considerably smaller in some cases. Defaults to false.") + + " These classes will not be part of the JaCoCo XML report at all, making it considerably smaller in some cases. Defaults to false. " + + "Has no effect when the output format is Teamscale Compact Coverage, which never includes uncovered classes.") ) var shouldIgnoreUncoveredClasses = false diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/convert/Converter.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/convert/Converter.kt index b5fedefdd..ee9bd70f7 100644 --- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/convert/Converter.kt +++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/convert/Converter.kt @@ -4,10 +4,10 @@ import com.teamscale.client.TestDetails import com.teamscale.jacoco.agent.benchmark import com.teamscale.jacoco.agent.logging.LoggingUtils import com.teamscale.jacoco.agent.options.AgentOptionParseException +import com.teamscale.jacoco.agent.options.EAgentReportFormat import com.teamscale.report.ReportUtils import com.teamscale.report.ReportUtils.listFiles import com.teamscale.report.jacoco.EmptyReportException -import com.teamscale.report.jacoco.JaCoCoXmlReportGenerator import com.teamscale.report.testwise.ETestArtifactFormat import com.teamscale.report.testwise.TestwiseCoverageReportWriter import com.teamscale.report.testwise.jacoco.JaCoCoTestwiseReportGenerator @@ -28,11 +28,20 @@ class Converter /** The command line arguments. */ private val arguments: ConvertCommand ) { - /** Converts one .exec binary coverage file to XML. */ - @Throws(IOException::class) + /** + * Converts one .exec binary coverage file to a coverage report. The output format is selected based on the output + * file extension: `.xml` produces a JaCoCo XML report, `.json` produces a Teamscale Compact Coverage report. + */ + @Throws(IOException::class, AgentOptionParseException::class) fun runJaCoCoReportGeneration() { val logger = LoggingUtils.getLogger(this) - val generator = JaCoCoXmlReportGenerator( + val outputFile = Paths.get(arguments.outputFile).toFile() + val format = EAgentReportFormat.fromFileExtension(outputFile.extension) + ?: throw AgentOptionParseException( + "Unsupported output file extension '${outputFile.extension}' for '${arguments.outputFile}'. " + + "Use .xml for JaCoCo XML or .json for Teamscale Compact Coverage." + ) + val generator = format.createGenerator( arguments.getClassDirectoriesOrZips(), wildcardIncludeExcludeFilter, arguments.duplicateClassFileBehavior, @@ -42,8 +51,8 @@ class Converter val jacocoExecutionDataList = listFiles(ETestArtifactFormat.JACOCO, arguments.getInputFiles()) try { - benchmark("Generating the XML report") { - generator.convertExecFilesToReport(jacocoExecutionDataList, Paths.get(arguments.outputFile).toFile()) + benchmark("Generating the coverage report") { + generator.convertExecFilesToReport(jacocoExecutionDataList, outputFile) } } catch (e: EmptyReportException) { logger.warn("Converted report was empty.", e) diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/AgentOptions.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/AgentOptions.kt index 05daba11a..89342e3b1 100644 --- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/AgentOptions.kt +++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/AgentOptions.kt @@ -34,6 +34,7 @@ import com.teamscale.jacoco.agent.upload.teamscale.TeamscaleUploader import com.teamscale.jacoco.agent.util.AgentUtils import com.teamscale.jacoco.agent.util.AgentUtils.mainTempDirectory import com.teamscale.report.EDuplicateClassFileBehavior +import com.teamscale.report.jacoco.JaCoCoBasedReportGenerator import com.teamscale.report.util.ClasspathWildcardIncludeFilter import com.teamscale.report.util.ILogger import java.io.File @@ -111,6 +112,13 @@ open class AgentOptions(private val logger: ILogger) { @JvmField var mode = EMode.NORMAL + /** + * The report format the agent produces in NORMAL mode. + * Has no effect in TESTWISE mode, which always produces a testwise coverage JSON. + */ + @JvmField + var normalModeReportFormat = EAgentReportFormat.TEAMSCALE_COMPACT_COVERAGE + /** The interval in minutes for dumping XML data. */ @JvmField var dumpIntervalInMinutes = 480 @@ -419,7 +427,8 @@ open class AgentOptions(private val logger: ILogger) { EUploadMethod.ARTIFACTORY -> createArtifactoryUploader(instrumentation) EUploadMethod.AZURE_FILE_STORAGE -> AzureFileStorageUploader( azureFileStorageConfig, - additionalMetaDataFiles + additionalMetaDataFiles, + reportFormat ) EUploadMethod.SAP_NWDI_TEAMSCALE -> { logger.info("NWDI configuration detected. The Agent will try and auto-detect commit information by searching all profiled Jar/War/Ear/... files.") @@ -452,7 +461,7 @@ open class AgentOptions(private val logger: ILogger) { private fun createTeamscaleSingleProjectUploader(instrumentation: Instrumentation?): IUploader { if (teamscaleServer.hasCommitOrRevision()) { - return TeamscaleUploader(teamscaleServer) + return TeamscaleUploader(teamscaleServer, reportFormat) } val uploader = createDelayedSingleProjectTeamscaleUploader() @@ -478,7 +487,7 @@ open class AgentOptions(private val logger: ILogger) { private fun createTeamscaleMultiProjectUploader( instrumentation: Instrumentation? ): DelayedTeamscaleMultiProjectUploader { - val uploader = DelayedTeamscaleMultiProjectUploader { project, commitInfo -> + val uploader = DelayedTeamscaleMultiProjectUploader(reportFormat) { project, commitInfo -> if (commitInfo!!.preferCommitDescriptorOverRevision || isEmpty(commitInfo.revision)) { return@DelayedTeamscaleMultiProjectUploader teamscaleServer.withProjectAndCommit( project!!, @@ -544,7 +553,7 @@ open class AgentOptions(private val logger: ILogger) { } else { teamscaleServer.revision = projectAndCommit.commitInfo.revision } - TeamscaleUploader(teamscaleServer) + TeamscaleUploader(teamscaleServer, reportFormat) } private fun startMultiGitPropertiesFileSearchInJarFile( @@ -585,7 +594,8 @@ open class AgentOptions(private val logger: ILogger) { private fun createNwdiTeamscaleUploader(instrumentation: Instrumentation?): IUploader { val uploader = DelayedSapNwdiMultiUploader { commit, application -> TeamscaleUploader( - teamscaleServer.withProjectAndCommit(application.teamscaleProject, commit) + teamscaleServer.withProjectAndCommit(application.teamscaleProject, commit), + reportFormat ) } instrumentation?.addTransformer( @@ -599,7 +609,17 @@ open class AgentOptions(private val logger: ILogger) { private val reportFormat: EReportFormat get() = if (useTestwiseCoverageMode()) { EReportFormat.TESTWISE_COVERAGE - } else EReportFormat.JACOCO + } else normalModeReportFormat.reportFormat + + /** Creates the report generator used by the agent in NORMAL mode based on [normalModeReportFormat]. */ + fun createReportGenerator(logger: ILogger): JaCoCoBasedReportGenerator<*> = + normalModeReportFormat.createGenerator( + classDirectoriesOrZips, + locationIncludeFilter, + duplicateClassFileBehavior, + ignoreUncoveredClasses, + logger + ) /** * Creates a new file with the given prefix, extension and current timestamp and ensures that the parent folder diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/AgentOptionsParser.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/AgentOptionsParser.kt index b7d3e0e62..b8eff3c05 100644 --- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/AgentOptionsParser.kt +++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/AgentOptionsParser.kt @@ -311,6 +311,10 @@ class AgentOptionsParser @VisibleForTesting internal constructor( options.mode = parseEnumValue(key, value) return true } + "report-format" -> { + options.normalModeReportFormat = parseEnumValue(key, value) + return true + } "includes" -> { options.jacocoIncludes = value.replace(";".toRegex(), ":") return true diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/EAgentReportFormat.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/EAgentReportFormat.kt new file mode 100644 index 000000000..3708e516c --- /dev/null +++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/EAgentReportFormat.kt @@ -0,0 +1,53 @@ +package com.teamscale.jacoco.agent.options + +import com.teamscale.client.EReportFormat +import com.teamscale.report.EDuplicateClassFileBehavior +import com.teamscale.report.compact.CompactCoverageReportGenerator +import com.teamscale.report.jacoco.JaCoCoBasedReportGenerator +import com.teamscale.report.jacoco.JaCoCoXmlReportGenerator +import com.teamscale.report.util.ClasspathWildcardIncludeFilter +import com.teamscale.report.util.ILogger +import java.io.File + +/** + * The coverage report format the agent produces in NORMAL mode. + * + * Each entry bundles the downstream [EReportFormat] used during upload with the on-disk file naming + * convention so that filename, format identifier, and zip-entry name cannot drift apart. + */ +enum class EAgentReportFormat( + /** The [EReportFormat] identifier sent to the upload backend. */ + @JvmField val reportFormat: EReportFormat, + /** Filename prefix for files written to the output directory. */ + @JvmField val fileNamePrefix: String, + /** Filename extension (without leading dot) for files written to the output directory. */ + @JvmField val fileExtension: String, +) { + /** Teamscale Compact Coverage (JSON). */ + TEAMSCALE_COMPACT_COVERAGE(EReportFormat.TEAMSCALE_COMPACT_COVERAGE, "compact-coverage", "json"), + /** JaCoCo XML. */ + JACOCO(EReportFormat.JACOCO, "jacoco", "xml"); + + /** Creates the report generator that produces this format from JaCoCo binary execution data. */ + fun createGenerator( + classDirectoriesOrArchives: List, + locationIncludeFilter: ClasspathWildcardIncludeFilter, + duplicateClassFileBehavior: EDuplicateClassFileBehavior, + ignoreUncoveredClasses: Boolean, + logger: ILogger + ): JaCoCoBasedReportGenerator<*> = when (this) { + TEAMSCALE_COMPACT_COVERAGE -> CompactCoverageReportGenerator( + classDirectoriesOrArchives, locationIncludeFilter, duplicateClassFileBehavior, logger + ) + JACOCO -> JaCoCoXmlReportGenerator( + classDirectoriesOrArchives, locationIncludeFilter, duplicateClassFileBehavior, ignoreUncoveredClasses, logger + ) + } + + companion object { + /** Returns the format whose [fileExtension] matches the given extension (case-insensitive), or `null`. */ + @JvmStatic + fun fromFileExtension(extension: String): EAgentReportFormat? = + entries.firstOrNull { it.fileExtension.equals(extension, ignoreCase = true) } + } +} diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/HttpZipUploaderBase.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/HttpZipUploaderBase.kt index 53f11124f..11a1c10b1 100644 --- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/HttpZipUploaderBase.kt +++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/HttpZipUploaderBase.kt @@ -138,6 +138,6 @@ abstract class HttpZipUploaderBase } protected open fun getZipEntryCoverageFileName(coverageFile: CoverageFile): String { - return "coverage.xml" + return coverageFile.name } } diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/azure/AzureFileStorageUploader.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/azure/AzureFileStorageUploader.kt index 5784832c5..5c3580064 100644 --- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/azure/AzureFileStorageUploader.kt +++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/azure/AzureFileStorageUploader.kt @@ -30,7 +30,8 @@ import java.util.regex.Pattern /** Uploads the coverage archive to a provided azure file storage. */ class AzureFileStorageUploader( config: AzureFileStorageConfig, - additionalMetaDataFiles: List + additionalMetaDataFiles: List, + private val reportFormat: EReportFormat ) : HttpZipUploaderBase( config.url!!, additionalMetaDataFiles, @@ -139,7 +140,7 @@ class AzureFileStorageUploader( /** Creates a file name for the zip-archive containing the coverage. */ private fun createFileName() = - "${EReportFormat.JACOCO.name.lowercase(Locale.getDefault())}-${System.currentTimeMillis()}.zip" + "${reportFormat.name.lowercase(Locale.getDefault())}-${System.currentTimeMillis()}.zip" /** Checks if the file with the given name exists */ @Throws(IOException::class, UploaderException::class) diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/delay/DelayedUploader.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/delay/DelayedUploader.kt index 43e5a0500..26054b622 100644 --- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/delay/DelayedUploader.kt +++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/delay/DelayedUploader.kt @@ -1,6 +1,7 @@ package com.teamscale.jacoco.agent.upload.delay import com.teamscale.jacoco.agent.logging.LoggingUtils.getLogger +import com.teamscale.jacoco.agent.options.EAgentReportFormat import com.teamscale.jacoco.agent.upload.IUploader import com.teamscale.jacoco.agent.util.DaemonThreadFactory import com.teamscale.report.jacoco.CoverageFile @@ -90,17 +91,19 @@ class DelayedUploader internal constructor( private fun uploadCachedXmls() { try { if (!cacheDir.isDirectory()) { - // Found data before XML was dumped + // Found data before any coverage was dumped return } - val xmlFiles = cacheDir.listDirectoryEntries().filter { path -> + val coverageFiles = cacheDir.listDirectoryEntries().filter { path -> val fileName = path.fileName.toString() - fileName.startsWith("jacoco-") && fileName.endsWith(".xml") + EAgentReportFormat.entries.any { format -> + fileName.startsWith("${format.fileNamePrefix}-") && fileName.endsWith(".${format.fileExtension}") + } } - xmlFiles.forEach { path -> wrappedUploader?.upload(CoverageFile(path.toFile())) } - logger.debug("Finished upload of cached XMLs to {}", wrappedUploader?.describe()) + coverageFiles.forEach { path -> wrappedUploader?.upload(CoverageFile(path.toFile())) } + logger.debug("Finished upload of cached coverage files to {}", wrappedUploader?.describe()) } catch (e: IOException) { - logger.error("Failed to list cached coverage XML files in {}", cacheDir.toAbsolutePath(), e) + logger.error("Failed to list cached coverage files in {}", cacheDir.toAbsolutePath(), e) } } } diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/teamscale/DelayedTeamscaleMultiProjectUploader.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/teamscale/DelayedTeamscaleMultiProjectUploader.kt index a1669984e..23866b34b 100644 --- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/teamscale/DelayedTeamscaleMultiProjectUploader.kt +++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/teamscale/DelayedTeamscaleMultiProjectUploader.kt @@ -1,5 +1,6 @@ package com.teamscale.jacoco.agent.upload.teamscale +import com.teamscale.client.EReportFormat import com.teamscale.client.TeamscaleServer import com.teamscale.jacoco.agent.commit_resolution.git_properties.CommitInfo import com.teamscale.jacoco.agent.options.ProjectAndCommit @@ -9,6 +10,7 @@ import java.io.File /** Wrapper for [TeamscaleUploader] that allows to upload the same coverage file to multiple Teamscale projects. */ class DelayedTeamscaleMultiProjectUploader( + private val reportFormat: EReportFormat, private val teamscaleServerFactory: (String?, CommitInfo?) -> TeamscaleServer ) : DelayedMultiUploaderBase(), IUploader { @JvmField @@ -31,7 +33,7 @@ class DelayedTeamscaleMultiProjectUploader( ) return } - teamscaleUploaders.add(TeamscaleUploader(teamscaleServer)) + teamscaleUploaders.add(TeamscaleUploader(teamscaleServer, reportFormat)) } override val wrappedUploaders: MutableCollection diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/teamscale/TeamscaleUploader.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/teamscale/TeamscaleUploader.kt index 87ce3b8b7..c2a1d7fe3 100644 --- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/teamscale/TeamscaleUploader.kt +++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/teamscale/TeamscaleUploader.kt @@ -8,6 +8,7 @@ import com.teamscale.client.StringUtils.emptyToNull import com.teamscale.client.StringUtils.nullToEmpty import com.teamscale.jacoco.agent.benchmark import com.teamscale.jacoco.agent.logging.LoggingUtils.getLogger +import com.teamscale.jacoco.agent.options.EAgentReportFormat import com.teamscale.jacoco.agent.upload.IUploadRetry import com.teamscale.jacoco.agent.upload.IUploader import com.teamscale.jacoco.agent.util.AgentUtils @@ -19,14 +20,15 @@ import java.nio.charset.StandardCharsets import java.nio.file.Files import java.util.* -/** Uploads XML Coverage to a Teamscale instance. */ +/** Uploads coverage reports to a Teamscale instance. */ class TeamscaleUploader( - @JvmField val teamscaleServer: TeamscaleServer + @JvmField val teamscaleServer: TeamscaleServer, + private val reportFormat: EReportFormat ) : IUploader, IUploadRetry { private val logger = getLogger(this) override fun upload(coverageFile: CoverageFile) { - doUpload(coverageFile, teamscaleServer) + doUpload(coverageFile, teamscaleServer, reportFormat) } override fun reupload(coverageFile: CoverageFile, properties: Properties) { @@ -40,12 +42,17 @@ class TeamscaleUploader( server.userName = teamscaleServer.userName server.url = teamscaleServer.url server.message = properties.getProperty(ETeamscaleServerProperties.MESSAGE.name) - doUpload(coverageFile, server) + // Infer the format from the on-disk file extension so retries survive a change in the + // agent's default report format between runs. Fall back to JACOCO for legacy files + // without one of the known coverage extensions. + val retryFormat = EAgentReportFormat.fromFileExtension(coverageFile.extension)?.reportFormat + ?: EReportFormat.JACOCO + doUpload(coverageFile, server, retryFormat) } - private fun doUpload(coverageFile: CoverageFile, teamscaleServer: TeamscaleServer) { + private fun doUpload(coverageFile: CoverageFile, teamscaleServer: TeamscaleServer, format: EReportFormat) { benchmark("Uploading report to Teamscale") { - if (tryUploading(coverageFile, teamscaleServer)) { + if (tryUploading(coverageFile, teamscaleServer, format)) { deleteCoverageFile(coverageFile) } else { logger.warn( @@ -111,8 +118,12 @@ class TeamscaleUploader( } /** Performs the upload and returns `true` if successful. */ - private fun tryUploading(coverageFile: CoverageFile, teamscaleServer: TeamscaleServer): Boolean { - logger.debug("Uploading JaCoCo artifact to {}", teamscaleServer) + private fun tryUploading( + coverageFile: CoverageFile, + teamscaleServer: TeamscaleServer, + format: EReportFormat + ): Boolean { + logger.debug("Uploading {} report to {}", format.readableName, teamscaleServer) try { // Cannot be executed in the constructor as this causes issues in WildFly server @@ -126,7 +137,7 @@ class TeamscaleUploader( teamscaleServer.project!!, teamscaleServer.commit, teamscaleServer.revision, - teamscaleServer.repository, teamscaleServer.partition!!, EReportFormat.JACOCO, + teamscaleServer.repository, teamscaleServer.partition!!, format, teamscaleServer.message!!, coverageFile.createFormRequestBody() ) return true diff --git a/agent/src/test/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitMultiProjectPropertiesLocatorTest.kt b/agent/src/test/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitMultiProjectPropertiesLocatorTest.kt index 2bc7d41ad..78e06f69a 100644 --- a/agent/src/test/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitMultiProjectPropertiesLocatorTest.kt +++ b/agent/src/test/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitMultiProjectPropertiesLocatorTest.kt @@ -1,6 +1,7 @@ package com.teamscale.jacoco.agent.commit_resolution.git_properties import com.teamscale.client.CommitDescriptor +import com.teamscale.client.EReportFormat import com.teamscale.client.TeamscaleServer import com.teamscale.jacoco.agent.options.ProjectAndCommit import com.teamscale.jacoco.agent.upload.teamscale.DelayedTeamscaleMultiProjectUploader @@ -13,7 +14,7 @@ internal class GitMultiProjectPropertiesLocatorTest { fun testNoErrorIsThrownWhenGitPropertiesFileDoesNotHaveAProject() { val projectAndCommits = mutableListOf() val locator = GitMultiProjectPropertiesLocator( - DelayedTeamscaleMultiProjectUploader { project, revision -> + DelayedTeamscaleMultiProjectUploader(EReportFormat.JACOCO) { project, revision -> projectAndCommits.add(ProjectAndCommit(project, revision)) TeamscaleServer() }, true, null @@ -26,7 +27,7 @@ internal class GitMultiProjectPropertiesLocatorTest { @Test fun testNoMultipleUploadsToSameProjectAndRevision() { - val delayedTeamscaleMultiProjectUploader = DelayedTeamscaleMultiProjectUploader { project, revision -> + val delayedTeamscaleMultiProjectUploader = DelayedTeamscaleMultiProjectUploader(EReportFormat.JACOCO) { project, revision -> val server = TeamscaleServer() server.project = project server.revision = revision!!.revision diff --git a/agent/src/test/kotlin/com/teamscale/jacoco/agent/convert/ConverterTest.kt b/agent/src/test/kotlin/com/teamscale/jacoco/agent/convert/ConverterTest.kt index 596d423ae..e11b3f1aa 100644 --- a/agent/src/test/kotlin/com/teamscale/jacoco/agent/convert/ConverterTest.kt +++ b/agent/src/test/kotlin/com/teamscale/jacoco/agent/convert/ConverterTest.kt @@ -32,6 +32,53 @@ class ConverterTest { .contains("TestClass") } + /** + * Ensures that running the converter with a `.json` output file produces a Teamscale Compact Coverage report. + */ + @Test + @Throws(Exception::class) + fun testCompactCoverageSmokeTest(@TempDir tempDir: File?) { + val execFile = File(javaClass.getResource("coverage.exec")!!.toURI()) + val classFile = File(javaClass.getResource("TestClass.class")!!.toURI()) + val outputFile = File(tempDir, "coverage.json") + + val arguments = ConvertCommand() + arguments.inputFiles = mutableListOf(execFile.absolutePath) + arguments.outputFile = outputFile.absolutePath + arguments.classDirectoriesOrZips = mutableListOf(classFile.absolutePath) + + Converter(arguments).runJaCoCoReportGeneration() + + val json = outputFile.readText() + Assertions.assertThat(json).isNotEmpty() + .contains("\"version\"") + .contains("\"coverage\"") + .contains("\"filePath\"") + .contains("TestClass") + .contains("\"fullyCoveredLines\":\"2,5-7,10-11\"") + } + + /** + * Ensures that the converter rejects unknown output file extensions with a helpful error. + */ + @Test + @Throws(Exception::class) + fun testRejectsUnknownOutputExtension(@TempDir tempDir: File?) { + val execFile = File(javaClass.getResource("coverage.exec")!!.toURI()) + val classFile = File(javaClass.getResource("TestClass.class")!!.toURI()) + val outputFile = File(tempDir, "coverage.txt") + + val arguments = ConvertCommand() + arguments.inputFiles = mutableListOf(execFile.absolutePath) + arguments.outputFile = outputFile.absolutePath + arguments.classDirectoriesOrZips = mutableListOf(classFile.absolutePath) + + Assertions.assertThatThrownBy { Converter(arguments).runJaCoCoReportGeneration() } + .hasMessageContaining("Unsupported output file extension") + .hasMessageContaining(".xml") + .hasMessageContaining(".json") + } + @Test @Throws(Exception::class) fun testNestedJar(@TempDir tempDir: File?) { diff --git a/agent/src/test/kotlin/com/teamscale/jacoco/agent/options/AgentOptionsParserTest.kt b/agent/src/test/kotlin/com/teamscale/jacoco/agent/options/AgentOptionsParserTest.kt index bb8922633..6fe1e32e1 100644 --- a/agent/src/test/kotlin/com/teamscale/jacoco/agent/options/AgentOptionsParserTest.kt +++ b/agent/src/test/kotlin/com/teamscale/jacoco/agent/options/AgentOptionsParserTest.kt @@ -316,6 +316,34 @@ class AgentOptionsParserTest { ).isEqualTo("commandlinetoken") } + @Test + @Throws(Exception::class) + fun reportFormatDefaultsToCompact() { + Assertions.assertThat(parseAndThrow("").normalModeReportFormat) + .isEqualTo(EAgentReportFormat.TEAMSCALE_COMPACT_COVERAGE) + } + + @Test + @Throws(Exception::class) + fun reportFormatCanBeOverriddenToJacoco() { + Assertions.assertThat(parseAndThrow("report-format=JACOCO").normalModeReportFormat) + .isEqualTo(EAgentReportFormat.JACOCO) + // Lowercase and dash-normalized values are accepted. + Assertions.assertThat(parseAndThrow("report-format=jacoco").normalModeReportFormat) + .isEqualTo(EAgentReportFormat.JACOCO) + Assertions.assertThat(parseAndThrow("report-format=teamscale-compact-coverage").normalModeReportFormat) + .isEqualTo(EAgentReportFormat.TEAMSCALE_COMPACT_COVERAGE) + } + + @Test + @Throws(Exception::class) + fun reportFormatRejectsUnknownValue() { + Assertions.assertThatThrownBy { parseAndThrow("report-format=xml") } + .hasMessageContaining("Invalid value for option `report-format`") + .hasMessageContaining("TEAMSCALE_COMPACT_COVERAGE") + .hasMessageContaining("JACOCO") + } + @Test @Throws(Exception::class) fun notGivingAnyOptionsShouldBeOK() { diff --git a/agent/src/test/kotlin/com/teamscale/jacoco/agent/upload/teamscale/TeamscaleAutomaticUploadRetryTest.kt b/agent/src/test/kotlin/com/teamscale/jacoco/agent/upload/teamscale/TeamscaleAutomaticUploadRetryTest.kt index 373f6b948..0a366fba7 100644 --- a/agent/src/test/kotlin/com/teamscale/jacoco/agent/upload/teamscale/TeamscaleAutomaticUploadRetryTest.kt +++ b/agent/src/test/kotlin/com/teamscale/jacoco/agent/upload/teamscale/TeamscaleAutomaticUploadRetryTest.kt @@ -1,6 +1,7 @@ package com.teamscale.jacoco.agent.upload.teamscale import com.teamscale.client.CommitDescriptor.Companion.parse +import com.teamscale.client.EReportFormat import com.teamscale.client.TeamscaleServer import com.teamscale.jacoco.agent.options.TestAgentOptionsBuilder import com.teamscale.jacoco.agent.upload.UploadTestBase @@ -30,7 +31,7 @@ class TeamscaleAutomaticUploadRetryTest : UploadTestBase() { server.commit = parse("master:HEAD") server.userName = "Foo" server.userAccessToken = "Token" - uploader = TeamscaleUploader(server) + uploader = TeamscaleUploader(server, EReportFormat.JACOCO) mockWebServer!!.enqueue(MockResponse().setResponseCode(400)) // This is expected to fail and leave the coverage on disk. uploader!!.upload(coverageFile!!) 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 index e4d95b988..12efafb4c 100644 --- a/report-generator/src/main/kotlin/com/teamscale/report/jacoco/CoverageFile.kt +++ b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/CoverageFile.kt @@ -52,6 +52,10 @@ data class CoverageFile(private val coverageFile: File) { val name: String get() = coverageFile.name + /** Get the filename extension of the coverage file (without leading dot, lowercase). */ + val extension: String + get() = coverageFile.extension.lowercase() + /** * Delete the coverage file from disk */ diff --git a/system-tests/api-changing-settings-should-dump/build.gradle.kts b/system-tests/api-changing-settings-should-dump/build.gradle.kts index a6081bc88..92d15c2fa 100644 --- a/system-tests/api-changing-settings-should-dump/build.gradle.kts +++ b/system-tests/api-changing-settings-should-dump/build.gradle.kts @@ -15,6 +15,7 @@ tasks.test { "teamscale-partition" to "partition_before_change", "teamscale-commit" to "master:12345", "includes" to "*SystemUnderTest*", + "report-format" to "JACOCO", ) ) } diff --git a/system-tests/default-excludes-test/src/test/kotlin/com/teamscale/tia/client/DefaultExcludesSystemTest.kt b/system-tests/default-excludes-test/src/test/kotlin/com/teamscale/tia/client/DefaultExcludesSystemTest.kt index a48387fb3..087810c16 100644 --- a/system-tests/default-excludes-test/src/test/kotlin/com/teamscale/tia/client/DefaultExcludesSystemTest.kt +++ b/system-tests/default-excludes-test/src/test/kotlin/com/teamscale/tia/client/DefaultExcludesSystemTest.kt @@ -1,6 +1,5 @@ package com.teamscale.tia.client -import com.teamscale.client.EReportFormat import com.teamscale.test.commons.SystemTestUtils import com.teamscale.test.commons.SystemTestUtils.dumpCoverage import com.teamscale.test.commons.TeamscaleMockServer @@ -11,6 +10,8 @@ import systemundertest.SystemUnderTest /** * Runs the system under test and then forces a dump of the agent to our [TeamscaleMockServer]. Checks the * resulting report to ensure the default excludes are applied. + * + * This test also acts as the end-to-end smoke test for the agent's default report format (Teamscale Compact Coverage). */ class DefaultExcludesSystemTest { @Test @@ -26,8 +27,14 @@ class DefaultExcludesSystemTest { SystemUnderTest().foo() dumpCoverage(SystemTestUtils.AGENT_PORT) - val report = teamscaleMockServer.getOnlyReport("part", EReportFormat.JACOCO) - assertThat(report).doesNotContain("shadow", "junit", "eclipse", "apache", "javax", "slf4j", "com/sun") - assertThat(report).contains("SystemUnderTest", "NotExcludedClass") + val report = teamscaleMockServer.getSession("part").getCompactCoverageReport(0) + ?: error("Expected a Teamscale Compact Coverage report to be uploaded") + val filePaths = report.coverage.map { it.filePath } + assertThat(filePaths) + .noneMatch { it.contains("shadow") || it.contains("junit") || it.contains("eclipse") } + .noneMatch { it.contains("apache") || it.contains("javax") || it.contains("slf4j") } + .noneMatch { it.contains("com/sun") } + .anyMatch { it.contains("SystemUnderTest") } + .anyMatch { it.contains("NotExcludedClass") } } } diff --git a/system-tests/kotlin-inline-function-test/build.gradle.kts b/system-tests/kotlin-inline-function-test/build.gradle.kts index ad05829d7..b2127d7aa 100644 --- a/system-tests/kotlin-inline-function-test/build.gradle.kts +++ b/system-tests/kotlin-inline-function-test/build.gradle.kts @@ -15,6 +15,7 @@ tasks.test { "teamscale-partition" to "part", "teamscale-commit" to "master:12345", "includes" to "*foo*", + "report-format" to "JACOCO", ) ) }