Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
62 changes: 52 additions & 10 deletions agent/src/main/kotlin/com/teamscale/jacoco/agent/util/AgentUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,23 @@ 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
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

Expand Down Expand Up @@ -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<String>): 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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
Loading