diff --git a/CHANGELOG.md b/CHANGELOG.md index a8520dc37..3cc99c090 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 +- [fix] _teamscale-jacoco-agent_: The profiler no longer aborts the JVM during startup under a class loader that records no local file for its own JAR, such as IntelliJ's `PathClassLoader`. It falls back to the JAR named by the `-javaagent` argument. - [fix] _impacted-test-engine_: Tests in a `@ParameterizedClass` are now collected. - [fix] _impacted-test-engine_: A test failure inside a nested test container (e.g. an invocation of a `@ParameterizedClass`) is no longer swallowed. - [fix] _teamscale-jacoco-agent_, _teamscale-maven-plugin_: A test that was executed more than once (e.g. once per parameter set of a `@ParameterizedClass`) is now reported once in the testwise coverage report, with the coverage of all of its executions merged, their durations summed up and the most severe of their results. Previously each execution overwrote the previous one. diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/util/AgentUtils.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/util/AgentUtils.kt index 2c707190c..5eb90439c 100644 --- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/util/AgentUtils.kt +++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/util/AgentUtils.kt @@ -5,8 +5,11 @@ import com.teamscale.client.FileSystemUtils import com.teamscale.client.TeamscaleServiceGenerator import com.teamscale.jacoco.agent.PreMain import com.teamscale.jacoco.agent.configuration.ProcessInformationRetriever +import org.jetbrains.annotations.VisibleForTesting import java.io.IOException +import java.lang.management.ManagementFactory import java.net.URISyntaxException +import java.net.URL import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths @@ -14,6 +17,11 @@ import java.util.* /** General utilities for working with the agent. */ object AgentUtils { + private const val JAVAAGENT_ARGUMENT_PREFIX = "-javaagent:" + + /** The agent's JAR, named as the installer ships it or as a build tool resolves it, with a version suffix. */ + private val AGENT_JAR_PATTERN = Regex("""teamscale-jacoco-agent(-.+)?\.jar""") + /** Version of this program. */ val VERSION: String @@ -43,20 +51,54 @@ object AgentUtils { /** Returns the directory that contains the agent installation. */ val agentDirectory: Path by lazy { - try { - val jarFileUri = PreMain::class.java.getProtectionDomain().codeSource.location.toURI() - // we assume that the dist zip is extracted and the agent jar not moved - val jarDirectory = Paths.get(jarFileUri).parent - jarDirectory.parent ?: jarDirectory // happens when the jar file is stored in the root directory - } catch (e: URISyntaxException) { - throw RuntimeException( - "Failed to obtain the agent's installation directory from its JAR URL." + - " ${BugReportMessages.REPORT_TO_CQSE}", - e + // we assume that the dist zip is extracted and the agent jar not moved + val jarDirectory = agentJarFile().toAbsolutePath().parent + jarDirectory.parent ?: jarDirectory // happens when the jar file is stored in the root directory + } + + /** Returns the agent's own JAR, or throws if neither of the two ways of locating it yields a file. */ + private fun agentJarFile(): Path = + agentJarFileFromCodeSource() + ?: agentJarFileFromJavaAgentJvmArgument() + ?: throw RuntimeException( + "Failed to locate the agent's own JAR, neither through its class loader nor the -javaagent argument." + + " ${BugReportMessages.REPORT_TO_CQSE}" ) + + /** Returns the agent's JAR as its own class loader records it, or null if that yields no local file. */ + private fun agentJarFileFromCodeSource(): Path? = + agentJarFileFromCodeSource(PreMain::class.java.protectionDomain?.codeSource?.location) + + /** + * Returns the agent's JAR for the given code source location, or null if the location is absent or names + * something other than a local file. Not every class loader records where it loaded a class from, and not + * every one loads it from a local file. + */ + @VisibleForTesting + internal fun agentJarFileFromCodeSource(codeSourceLocation: URL?): Path? { + val jarFileUri = try { + codeSourceLocation?.toURI() ?: return null + } catch (_: URISyntaxException) { + return null } + // Paths.get rejects every other scheme. + if (jarFileUri.scheme != "file") return null + return Paths.get(jarFileUri) } + /** Returns the agent's JAR as the JVM's own `-javaagent` arguments name it, or null if none of them names it. */ + private fun agentJarFileFromJavaAgentJvmArgument(): Path? = + agentJarFileFromJavaAgentJvmArgument(ManagementFactory.getRuntimeMXBean().inputArguments) + + /** Returns the agent's JAR as the given `-javaagent` arguments name it, or null if none of them names it. */ + @VisibleForTesting + internal fun agentJarFileFromJavaAgentJvmArgument(jvmArguments: List): Path? = + jvmArguments + .filter { it.startsWith(JAVAAGENT_ARGUMENT_PREFIX) } + .map { Paths.get(it.removePrefix(JAVAAGENT_ARGUMENT_PREFIX).substringBefore('=')) } + .filter { AGENT_JAR_PATTERN.matches(it.fileName?.toString().orEmpty()) } + .firstOrNull() + init { val bundle = ResourceBundle.getBundle("com.teamscale.jacoco.agent.app") VERSION = bundle.getString("version") diff --git a/agent/src/test/kotlin/com/teamscale/jacoco/agent/util/AgentUtilsTest.kt b/agent/src/test/kotlin/com/teamscale/jacoco/agent/util/AgentUtilsTest.kt new file mode 100644 index 000000000..8115f3996 --- /dev/null +++ b/agent/src/test/kotlin/com/teamscale/jacoco/agent/util/AgentUtilsTest.kt @@ -0,0 +1,75 @@ +package com.teamscale.jacoco.agent.util + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import java.net.URI +import java.nio.file.Paths + +/** Tests the [AgentUtils]. */ +class AgentUtilsTest { + @Test + fun findsAgentJarAtItsCodeSourceLocation() { + val agentJar = AgentUtils.agentJarFileFromCodeSource( + URI.create("file:/opt/profiler/lib/teamscale-jacoco-agent.jar").toURL() + ) + + assertThat(agentJar).isEqualTo(Paths.get("/opt/profiler/lib/teamscale-jacoco-agent.jar")) + } + + @Test + fun findsNoAgentJarWithoutCodeSourceLocation() { + val agentJar = AgentUtils.agentJarFileFromCodeSource(null) + + assertThat(agentJar).isNull() + } + + @Test + fun findsNoAgentJarAtNonFileCodeSourceLocation() { + val agentJar = AgentUtils.agentJarFileFromCodeSource( + URI.create("jar:file:/opt/app.jar!/lib/teamscale-jacoco-agent.jar").toURL() + ) + + assertThat(agentJar).isNull() + } + + @Test + fun findsAgentJarAmongOtherJavaAgents() { + val agentJar = AgentUtils.agentJarFileFromJavaAgentJvmArgument( + listOf( + "-Dfoo=bar", + "-javaagent:/tmp/mockito-core-5.23.0.jar", + "-javaagent:/opt/profiler/lib/teamscale-jacoco-agent-38.1.0.jar=mode=testwise,out=/tmp/out", + "-Xmx1g" + ) + ) + + assertThat(agentJar).isEqualTo(Paths.get("/opt/profiler/lib/teamscale-jacoco-agent-38.1.0.jar")) + } + + @Test + fun findsAgentJarWithoutVersionSuffix() { + val agentJar = AgentUtils.agentJarFileFromJavaAgentJvmArgument( + listOf("-javaagent:/opt/profiler/lib/teamscale-jacoco-agent.jar=mode=testwise,out=/tmp/out") + ) + + assertThat(agentJar).isEqualTo(Paths.get("/opt/profiler/lib/teamscale-jacoco-agent.jar")) + } + + @Test + fun findsAgentJarWithoutAgentOptions() { + val agentJar = AgentUtils.agentJarFileFromJavaAgentJvmArgument( + listOf("-javaagent:/opt/profiler/lib/teamscale-jacoco-agent-38.1.0.jar") + ) + + assertThat(agentJar).isEqualTo(Paths.get("/opt/profiler/lib/teamscale-jacoco-agent-38.1.0.jar")) + } + + @Test + fun findsNoAgentJarWhenNoJavaAgentNamesIt() { + val agentJar = AgentUtils.agentJarFileFromJavaAgentJvmArgument( + listOf("-javaagent:/tmp/mockito-core-5.23.0.jar") + ) + + assertThat(agentJar).isNull() + } +}