diff --git a/.idea/misc.xml b/.idea/misc.xml
index 86adf2483..272935ac9 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -30,7 +30,7 @@
-
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b7c6bbd00..c773cb0e5 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
+- [security fix] _agent_: The Azure shared-key error messages no longer include the configured `azure-key` value in the exception text (previously leaked the access key into stack traces and logs)
# 36.5.2
- [security fix] _agent_: The Teamscale access token was logged in clear text in DEBUG-level logs (e.g., when `debug=true` was set) and in the WARN-level log emitted when multiple `-javaagent` arguments are present. The token is now obfuscated in those logs as well, matching INFO-level behavior.
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 a0a0cfce6..e226b8a79 100644
--- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/Agent.kt
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/Agent.kt
@@ -92,7 +92,10 @@ class Agent(options: AgentOptions, instrumentation: Instrumentation?) : AgentBas
}
Files.deleteIfExists(file.toPath())
} catch (e: IOException) {
- logger.error("Reuploading coverage failed. $e")
+ logger.error(
+ "Reuploading cached coverage from {} failed. The file is kept and the upload will be retried on" +
+ " the next agent restart.", file, e
+ )
}
}
@@ -144,7 +147,10 @@ class Agent(options: AgentOptions, instrumentation: Instrumentation?) : AgentBas
try {
dump = controller.dumpAndReset()
} catch (e: JacocoRuntimeController.DumpException) {
- logger.error("Dumping failed, retrying later", e)
+ logger.error(
+ "Dumping coverage data failed. The agent will retry on the next dump interval." +
+ " If this persists, report a bug.", e
+ )
return
}
@@ -163,9 +169,17 @@ class Agent(options: AgentOptions, instrumentation: Instrumentation?) : AgentBas
uploader.upload(coverageFile)
}
} catch (e: IOException) {
- logger.error("Converting binary dump to XML failed", e)
+ logger.error(
+ "Converting the binary JaCoCo dump to XML failed. This dump's coverage will be skipped." +
+ " If you set 'class-dir', ensure it points to your compiled classes and they are readable.", e
+ )
} catch (e: EmptyReportException) {
- logger.error("No coverage was collected. ${e.message}", e)
+ logger.warn(
+ "No coverage was collected in this dump interval: {}." +
+ " This is normal if no profiled code ran." +
+ " Check the 'includes'/'excludes' patterns if you expected coverage.",
+ e.message
+ )
}
}
}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/AgentBase.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/AgentBase.kt
index d572bd3c0..32b9175c7 100644
--- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/AgentBase.kt
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/AgentBase.kt
@@ -60,8 +60,11 @@ abstract class AgentBase(
try {
initServer()
} catch (e: Exception) {
- logger.error("Could not start http server on port $port. Please check if the port is blocked.")
- throw IllegalStateException("Control server not started.", e)
+ throw IllegalStateException(
+ "Could not start the agent control HTTP server on port $port." +
+ " Check that the port is not already in use and try a different value for the" +
+ " 'http-server-port' option.", e
+ )
}
}
}
@@ -117,7 +120,11 @@ abstract class AgentBase(
prepareShutdown()
logger.info("Teamscale Java Profiler successfully shut down.")
} catch (e: Exception) {
- logger.error("Exception during profiler shutdown.", e)
+ logger.error(
+ "Error while shutting down the Teamscale Java Profiler. Coverage may not have been flushed;" +
+ " check the log for details. If this keeps happening, report a bug.",
+ e
+ )
} finally {
// Try to flush logging resources also in case of an exception during shutdown
PreMain.closeLoggingResources()
@@ -131,7 +138,10 @@ abstract class AgentBase(
try {
server.stop()
} catch (e: Exception) {
- logger.error("Could not stop server so it is killed now.", e)
+ logger.error(
+ "Could not gracefully stop the agent control HTTP server on port {}; forcing shutdown." +
+ " Pending test events may be lost.", options.httpServerPort, e
+ )
} finally {
server.destroy()
}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/JacocoRuntimeController.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/JacocoRuntimeController.kt
index d62ee1eaa..5c10a5e9b 100644
--- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/JacocoRuntimeController.kt
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/JacocoRuntimeController.kt
@@ -1,5 +1,6 @@
package com.teamscale.jacoco.agent
+import com.teamscale.client.BugReportMessages
import com.teamscale.report.jacoco.dump.Dump
import org.jacoco.agent.rt.IAgent
import org.jacoco.core.data.ExecutionDataReader
@@ -47,7 +48,10 @@ class JacocoRuntimeController
}
}
} catch (e: IOException) {
- throw DumpException("should never happen for the ByteArrayInputStream", e)
+ throw DumpException(
+ "Unexpected IOException while reading the in-memory coverage dump." +
+ " ${BugReportMessages.REPORT_TO_CQSE}", e
+ )
}
}
@@ -75,7 +79,10 @@ class JacocoRuntimeController
try {
agent.dump(true)
} catch (e: IOException) {
- throw DumpException(e.message, e)
+ throw DumpException(
+ "Failed to dump execution data: ${e.message ?: e.javaClass.simpleName}." +
+ " The dump will be retried on the next interval.", e
+ )
}
}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/PreMain.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/PreMain.kt
index 333c33a33..e3c54c389 100644
--- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/PreMain.kt
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/PreMain.kt
@@ -247,7 +247,10 @@ object PreMain {
if (FileSystemUtils.isValidPath(logDirectory.toString()) && Files.isWritable(logDirectory)) {
logger.info("Logging to $logDirectory")
} else {
- logger.warn("Could not create $logDirectory. Logging to console only.")
+ logger.warn(
+ "Could not create debug log directory '$logDirectory'" +
+ " (check permissions and free disk space). Falling back to console-only logging."
+ )
}
}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitMultiProjectPropertiesLocator.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitMultiProjectPropertiesLocator.kt
index b54df0acb..411c08111 100644
--- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitMultiProjectPropertiesLocator.kt
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitMultiProjectPropertiesLocator.kt
@@ -87,9 +87,19 @@ class GitMultiProjectPropertiesLocator(
uploader.addTeamscaleProjectAndCommit(file, projectAndCommit)
}
} catch (e: IOException) {
- logger.error("Error during asynchronous search for git.properties in {}", file, e)
+ logger.error(
+ "Could not search for git.properties in {}." +
+ " Auto-detection of the Teamscale project/commit for code loaded from this location will be skipped" +
+ " — set 'teamscale-project' and 'teamscale-revision' manually if no other git.properties is found.",
+ file, e
+ )
} catch (e: InvalidGitPropertiesException) {
- logger.error("Error during asynchronous search for git.properties in {}", file, e)
+ logger.error(
+ "Could not search for git.properties in {}." +
+ " Auto-detection of the Teamscale project/commit for code loaded from this location will be skipped" +
+ " — set 'teamscale-project' and 'teamscale-revision' manually if no other git.properties is found.",
+ file, e
+ )
}
}
}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitPropertiesLocatingTransformer.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitPropertiesLocatingTransformer.kt
index a0e1671aa..fe9d84cf1 100644
--- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitPropertiesLocatingTransformer.kt
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitPropertiesLocatingTransformer.kt
@@ -69,7 +69,11 @@ class GitPropertiesLocatingTransformer(
} catch (e: Throwable) {
// we catch Throwable to be sure that we log all errors as anything thrown from this method is
// silently discarded by the JVM
- logger.error("Failed to process class {} in search of git.properties", className, e)
+ logger.error(
+ "Failed to process class {} while searching for git.properties. This class will be skipped" +
+ " — coverage detection should still work via other classes from the same JAR.",
+ className, e
+ )
}
return null
}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitPropertiesLocatorUtils.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitPropertiesLocatorUtils.kt
index 6e35d08f4..a3c566522 100644
--- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitPropertiesLocatorUtils.kt
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitPropertiesLocatorUtils.kt
@@ -261,7 +261,9 @@ object GitPropertiesLocatorUtils {
}
} catch (e: IOException) {
throw IOException(
- "Reading jar ${file.absolutePath} for obtaining commit descriptor from git.properties failed", e
+ "Reading JAR file ${file.absolutePath} for git.properties failed: ${e.message}." +
+ " Verify the file is a valid JAR and is readable by the user running the JVM.",
+ e
)
}
}
@@ -316,7 +318,9 @@ object GitPropertiesLocatorUtils {
}
} catch (e: IOException) {
throw IOException(
- "Reading directory ${gitPropertiesFile.absolutePath} for obtaining commit descriptor from git.properties failed", e
+ "Reading directory ${gitPropertiesFile.absolutePath} for git.properties failed: ${e.message}." +
+ " Verify the path is readable by the user running the JVM.",
+ e
)
}
}
@@ -359,7 +363,10 @@ object GitPropertiesLocatorUtils {
}
if (isEmpty && isRootArchive) {
- throw IOException("No entries in Jar file $archiveName. Is this a valid jar file?. If so, please report to CQSE.")
+ throw IOException(
+ "No entries found in JAR file $archiveName. Verify this is a valid JAR file." +
+ " If it is, report a bug."
+ )
}
return result
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitSingleProjectPropertiesLocator.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitSingleProjectPropertiesLocator.kt
index bd017d6e1..4217974af 100644
--- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitSingleProjectPropertiesLocator.kt
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitSingleProjectPropertiesLocator.kt
@@ -76,9 +76,19 @@ class GitSingleProjectPropertiesLocator(
jarFileWithGitProperties = file
uploader.setCommitAndTriggerAsynchronousUpload(dataEntry)
} catch (e: IOException) {
- logger.error("Error during asynchronous search for git.properties in {}", file.toString(), e)
+ logger.error(
+ "Could not search for git.properties in {}." +
+ " Auto-detection of the Teamscale project/commit for code loaded from this location will be skipped" +
+ " — set 'teamscale-project' and 'teamscale-revision' manually if no other git.properties is found.",
+ file.toString(), e
+ )
} catch (e: InvalidGitPropertiesException) {
- logger.error("Error during asynchronous search for git.properties in {}", file.toString(), e)
+ logger.error(
+ "Could not search for git.properties in {}." +
+ " Auto-detection of the Teamscale project/commit for code loaded from this location will be skipped" +
+ " — set 'teamscale-project' and 'teamscale-revision' manually if no other git.properties is found.",
+ file.toString(), e
+ )
}
}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/sapnwdi/NwdiMarkerClassLocatingTransformer.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/sapnwdi/NwdiMarkerClassLocatingTransformer.kt
index 3e94ebf18..5d45bc4e2 100644
--- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/sapnwdi/NwdiMarkerClassLocatingTransformer.kt
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/sapnwdi/NwdiMarkerClassLocatingTransformer.kt
@@ -64,7 +64,9 @@ class NwdiMarkerClassLocatingTransformer(
// we catch Throwable to be sure that we log all errors as anything thrown from this method is
// silently discarded by the JVM
logger.error(
- "Failed to process class {} trying to determine its last modification timestamp.", className, e
+ "Failed to determine the last-modified timestamp of class {} for SAP NWDI commit auto-detection." +
+ " This class will be skipped; auto-detection should still succeed via other marker classes.",
+ className, e
)
}
return null
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.kt
index ecdcc096b..ad863fdcf 100644
--- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.kt
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.kt
@@ -61,8 +61,10 @@ class ConfigurationViaTeamscale(
try {
val response = teamscaleClient.sendHeartbeat(profilerId!!, profilerInfo).execute()
if (!response.isSuccessful) {
- LoggingUtils.getLogger(this)
- .error("Failed to send heartbeat. Teamscale responded with: ${response.errorBody()?.string()}")
+ LoggingUtils.getLogger(this).error(
+ "Failed to send heartbeat to Teamscale: HTTP ${response.code()} ${response.errorBody()?.string()}." +
+ " If heartbeats keep failing, Teamscale will mark this profiler as offline."
+ )
}
} catch (e: IOException) {
LoggingUtils.getLogger(this).error("Failed to send heartbeat to Teamscale!", e)
@@ -78,11 +80,17 @@ class ConfigurationViaTeamscale(
response = teamscaleClient.unregisterProfilerLegacy(profilerId).execute()
}
if (!response.isSuccessful) {
- LoggingUtils.getLogger(this)
- .error("Failed to unregister profiler. Teamscale responded with: ${response.errorBody()?.string()}")
+ LoggingUtils.getLogger(this).error(
+ "Failed to unregister profiler with Teamscale: HTTP ${response.code()} ${response.errorBody()?.string()}." +
+ " The profiler will be marked offline automatically after the heartbeat timeout."
+ )
}
} catch (e: IOException) {
- LoggingUtils.getLogger(this).error("Failed to unregister profiler!", e)
+ LoggingUtils.getLogger(this).error(
+ "Failed to unregister profiler with Teamscale (network error)." +
+ " The profiler will be marked offline automatically after the heartbeat timeout.",
+ e
+ )
}
}
@@ -122,12 +130,10 @@ class ConfigurationViaTeamscale(
val body = response.body()
return parseProfilerRegistration(body!!, response, teamscaleClient, processInformation)
} catch (e: IOException) {
- // we include the causing error message in this exception's message since this causes it to be printed
- // to stderr which is much more helpful than just saying "something didn't work"
throw AgentOptionReceiveException(
- "Failed to retrieve profiler configuration from Teamscale due to network error: ${
- LoggingUtils.getStackTraceAsString(e)
- }", e
+ "Failed to retrieve profiler configuration from Teamscale due to network error: ${e.message}." +
+ " Verify the Teamscale URL ($url) is reachable from this machine and the configured user has access.",
+ e
)
}
}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/configuration/ProcessInformationRetriever.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/configuration/ProcessInformationRetriever.kt
index e4692acdd..4ad524ce7 100644
--- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/configuration/ProcessInformationRetriever.kt
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/configuration/ProcessInformationRetriever.kt
@@ -24,7 +24,11 @@ class ProcessInformationRetriever(private val logger: ILogger) {
try {
return InetAddress.getLocalHost().hostName
} catch (e: UnknownHostException) {
- logger.error("Failed to determine hostname!", e)
+ logger.warn(
+ "Could not determine the local hostname; using an empty value." +
+ " This only affects the display name shown in Teamscale's profiler list.",
+ e
+ )
return ""
}
}
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..3e4445b08 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
@@ -46,7 +46,11 @@ class Converter
generator.convertExecFilesToReport(jacocoExecutionDataList, Paths.get(arguments.outputFile).toFile())
}
} catch (e: EmptyReportException) {
- logger.warn("Converted report was empty.", e)
+ logger.warn(
+ "Converted report was empty — no coverage in the input .exec files." +
+ " If you set 'class-dir', verify it points to compiled classes matching the recorded coverage.",
+ e
+ )
}
}
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 7538d7014..44020f988 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
@@ -259,7 +259,10 @@ class AgentOptionsParser @VisibleForTesting internal constructor(
try {
options.additionalMetaDataFiles = splitMultiOptionValue(value).map { path -> Paths.get(path) }
} catch (e: InvalidPathException) {
- throw AgentOptionParseException("Invalid path given for option 'upload-metadata'", e)
+ throw AgentOptionParseException(
+ "Invalid path given for option 'upload-metadata': '$value' (${e.message})." +
+ " Use a semicolon-separated list of file paths.", e
+ )
}
return true
}
@@ -326,7 +329,9 @@ class AgentOptionsParser @VisibleForTesting internal constructor(
key, filePatternResolver, list
)
} catch (e: IOException) {
- throw AgentOptionParseException(e)
+ throw AgentOptionParseException(
+ "Failed to resolve class directories for option 'class-dir': ${e.message}", e
+ )
}
return true
}
@@ -504,7 +509,9 @@ class AgentOptionsParser @VisibleForTesting internal constructor(
try {
return filePatternResolver.parsePath(optionName, pattern)
} catch (e: IOException) {
- throw AgentOptionParseException(e)
+ throw AgentOptionParseException(
+ "Invalid path or pattern given for option '$optionName': '$pattern' (${e.message})", e
+ )
}
}
@@ -533,7 +540,10 @@ class AgentOptionsParser @VisibleForTesting internal constructor(
@Throws(AgentOptionParseException::class)
private fun getUrl(key: String?, value: String) =
- value.toHttpUrlOrNull() ?: throw AgentOptionParseException("Invalid URL given for option '$key'")
+ value.toHttpUrlOrNull() ?: throw AgentOptionParseException(
+ "Invalid URL given for option '$key': '$value'." +
+ " The URL must be of the form http(s)://host[:port]/."
+ )
/**
* Splits the given value at semicolons.
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/JacocoAgentOptionsBuilder.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/JacocoAgentOptionsBuilder.kt
index 78c7cd5e7..1ab15101a 100644
--- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/JacocoAgentOptionsBuilder.kt
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/JacocoAgentOptionsBuilder.kt
@@ -47,26 +47,40 @@ class JacocoAgentOptionsBuilder(private val agentOptions: AgentOptions) {
try {
return Files.createDirectory(mainTempDirectory.resolve("jacoco-class-dump"))
} catch (_: IOException) {
- logger.warn("Unable to create temporary directory in default location. Trying in system temp directory.")
+ logger.warn(
+ "Could not create class-dump directory under {}. Trying the system temp directory next.",
+ mainTempDirectory
+ )
}
try {
return Files.createTempDirectory("jacoco-class-dump")
} catch (_: IOException) {
- logger.warn("Unable to create temporary directory in default location. Trying in output directory.")
+ logger.warn(
+ "Could not create class-dump directory in the system temp directory ({}). Trying the agent's output directory next.",
+ System.getProperty("java.io.tmpdir")
+ )
}
try {
return Files.createTempDirectory(agentOptions.outputDirectory!!, "jacoco-class-dump")
} catch (_: IOException) {
- logger.warn("Unable to create temporary directory in output directory. Trying in agent's directory.")
+ logger.warn(
+ "Could not create class-dump directory under the agent output directory ({}). Trying the agent's install directory next.",
+ agentOptions.outputDirectory
+ )
}
val agentDirectory = agentDirectory
try {
return Files.createTempDirectory(agentDirectory, "jacoco-class-dump")
} catch (e: IOException) {
- throw AgentOptionParseException("Unable to create a temporary directory anywhere", e)
+ throw AgentOptionParseException(
+ "Could not create a class-dump directory under any of: ${mainTempDirectory}," +
+ " the system temp directory, ${agentOptions.outputDirectory}, or $agentDirectory." +
+ " Verify at least one of these locations is writable.",
+ e
+ )
}
}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/TeamscalePropertiesUtils.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/TeamscalePropertiesUtils.kt
index 4ea5ad4ff..97aa51fa8 100644
--- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/TeamscalePropertiesUtils.kt
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/TeamscalePropertiesUtils.kt
@@ -39,29 +39,42 @@ object TeamscalePropertiesUtils {
try {
val properties = readProperties(teamscalePropertiesPath.toFile())
- return parseProperties(properties)
+ return parseProperties(properties, teamscalePropertiesPath)
} catch (e: IOException) {
throw AgentOptionParseException("Failed to read $teamscalePropertiesPath", e)
}
}
@Throws(AgentOptionParseException::class)
- private fun parseProperties(properties: Properties): TeamscaleCredentials {
+ private fun parseProperties(properties: Properties, teamscalePropertiesPath: Path): TeamscaleCredentials {
val urlString = properties.getProperty("url")
- ?: throw AgentOptionParseException("teamscale.properties is missing the url field")
+ ?: throw AgentOptionParseException(
+ "teamscale.properties at $teamscalePropertiesPath is missing the 'url' field." +
+ " Add a line like 'url=https://teamscale.example.com/'."
+ )
val url: HttpUrl
try {
url = urlString.toHttpUrl()
} catch (e: IllegalArgumentException) {
- throw AgentOptionParseException("teamscale.properties contained malformed URL $urlString", e)
+ throw AgentOptionParseException(
+ "teamscale.properties at $teamscalePropertiesPath contains a malformed URL '$urlString'." +
+ " Expected format: 'https://teamscale.example.com/'.",
+ e
+ )
}
val userName = properties.getProperty("username")
- ?: throw AgentOptionParseException("teamscale.properties is missing the username field")
+ ?: throw AgentOptionParseException(
+ "teamscale.properties at $teamscalePropertiesPath is missing the 'username' field." +
+ " Add a line like 'username=alice'."
+ )
val accessKey = properties.getProperty("accesskey")
- ?: throw AgentOptionParseException("teamscale.properties is missing the accesskey field")
+ ?: throw AgentOptionParseException(
+ "teamscale.properties at $teamscalePropertiesPath is missing the 'accesskey' field." +
+ " Add a line like 'accesskey='."
+ )
return TeamscaleCredentials(url, userName, accessKey)
}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.kt
index b49fb0f64..68ddf321d 100644
--- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.kt
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.kt
@@ -38,7 +38,10 @@ class TeamscaleProxyOptions(private val protocol: ProxySystemProperties.Protocol
proxyPort = proxySystemProperties.proxyPort
} catch (e: ProxySystemProperties.IncorrectPortFormatException) {
proxyPort = -1
- logger.warn(e.message!!)
+ logger.warn(
+ "Ignoring invalid proxy port from system properties for protocol $protocol: ${e.message}." +
+ " The agent will continue without using the proxy until a valid port is configured."
+ )
}
proxyUser = proxySystemProperties.proxyUser
proxyPassword = proxySystemProperties.proxyPassword
@@ -108,7 +111,8 @@ class TeamscaleProxyOptions(private val protocol: ProxySystemProperties.Protocol
TeamscaleProxySystemProperties(protocol).proxyPassword = proxyPassword
} catch (e: IOException) {
logger.error(
- "Unable to open file containing proxy password. Please make sure the file exists and the user has the permissions to read the file.",
+ "Unable to read proxy password from $proxyPasswordFilePath." +
+ " Verify the file exists and is readable by the user running the JVM.",
e
)
}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/sapnwdi/DelayedSapNwdiMultiUploader.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/sapnwdi/DelayedSapNwdiMultiUploader.kt
index 1146931be..7e603ade1 100644
--- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/sapnwdi/DelayedSapNwdiMultiUploader.kt
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/sapnwdi/DelayedSapNwdiMultiUploader.kt
@@ -32,7 +32,12 @@ class DelayedSapNwdiMultiUploader(
private fun registerShutdownHook() {
Runtime.getRuntime().addShutdownHook(Thread {
if (wrappedUploaders.isEmpty()) {
- logger.error("The application was shut down before a commit could be found. The recorded coverage is lost.")
+ logger.error(
+ "The application was shut down before any commit could be found for the SAP NWDI marker classes." +
+ " The recorded coverage is lost." +
+ " Ensure every NWDI marker class is present in a JAR with a valid git.properties," +
+ " and enable debug logging via the 'logging-config' option to see which classes were inspected."
+ )
}
})
}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/sapnwdi/SapNwdiApplication.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/sapnwdi/SapNwdiApplication.kt
index b2a90718f..4501b5ff4 100644
--- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/sapnwdi/SapNwdiApplication.kt
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/sapnwdi/SapNwdiApplication.kt
@@ -22,7 +22,10 @@ data class SapNwdiApplication(
return markerClassAndProjectPairs.map { pair ->
if (pair.isBlank()) {
- throw AgentOptionParseException("Application definition is expected not to be empty.")
+ throw AgentOptionParseException(
+ "Empty entry in option 'sap-nwdi-applications'." +
+ " Provide entries in the form 'com.your.MarkerClass:teamscale-project-id', separated by ';'."
+ )
}
val parts = pair.split(":").dropLastWhile { it.isEmpty() }
@@ -34,12 +37,18 @@ data class SapNwdiApplication(
val markerClass = parts[0].trim()
if (markerClass.isEmpty()) {
- throw AgentOptionParseException("Marker class is not given for $pair!")
+ throw AgentOptionParseException(
+ "Option 'sap-nwdi-applications': no marker class given in entry '$pair'." +
+ " Use the form 'com.your.MarkerClass:teamscale-project-id'."
+ )
}
val teamscaleProject = parts[1].trim()
if (teamscaleProject.isEmpty()) {
- throw AgentOptionParseException("Teamscale project is not given for $pair!")
+ throw AgentOptionParseException(
+ "Option 'sap-nwdi-applications': no Teamscale project given in entry '$pair'." +
+ " Use the form 'com.your.MarkerClass:teamscale-project-id'."
+ )
}
SapNwdiApplication(markerClass, teamscaleProject)
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/testimpact/CoverageToExecFileStrategy.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/testimpact/CoverageToExecFileStrategy.kt
index 6eed9d8ac..900bef3b9 100644
--- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/testimpact/CoverageToExecFileStrategy.kt
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/testimpact/CoverageToExecFileStrategy.kt
@@ -36,7 +36,12 @@ class CoverageToExecFileStrategy(
testExecutionWriter?.append(testExecution)
logger.debug("Successfully wrote test execution for {}", test)
} catch (e: IOException) {
- logger.error("Failed to store test execution: {}", e.message, e)
+ logger.error(
+ "Failed to append test execution for test '{}' to the testwise report: {}." +
+ " This test will be missing from the final report." +
+ " Check that the agent's 'out' directory is writable.",
+ test, e.message, e
+ )
}
}
return null
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/DelayedMultiUploaderBase.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/DelayedMultiUploaderBase.kt
index 4be84c943..2b675d4bb 100644
--- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/DelayedMultiUploaderBase.kt
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/DelayedMultiUploaderBase.kt
@@ -17,7 +17,10 @@ abstract class DelayedMultiUploaderBase : IUploader {
val wrappedUploaders = this.wrappedUploaders
wrappedUploaders.forEach { _ -> coverageFile.acquireReference() }
if (wrappedUploaders.isEmpty()) {
- logger.warn("No commits have been found yet to which coverage should be uploaded. Discarding coverage")
+ logger.warn(
+ "No commits have been resolved yet — discarding this coverage dump." +
+ " Provide a 'git.properties' file in the profiled JARs or set 'teamscale-commit'/'teamscale-revision' to avoid this."
+ )
} else {
wrappedUploaders.forEach { wrappedUploader ->
wrappedUploader.upload(coverageFile)
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..06ea2c970 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
@@ -63,8 +63,11 @@ abstract class HttpZipUploaderBase
(this as? IUploadRetry)?.markFileForUploadRetry(coverageFile)
}
}
- } catch (_: IOException) {
- logger.warn("Could not delete file {} after upload", coverageFile)
+ } catch (e: IOException) {
+ logger.warn(
+ "Could not delete coverage file {} after a successful upload — you may delete it manually.",
+ coverageFile, e
+ )
}
}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/artifactory/ArtifactoryConfig.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/artifactory/ArtifactoryConfig.kt
index 34469f495..5a180c07a 100644
--- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/artifactory/ArtifactoryConfig.kt
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/artifactory/ArtifactoryConfig.kt
@@ -184,7 +184,11 @@ class ArtifactoryConfig {
gitPropertiesCommitTimeFormat
)
if (commitInfo.isEmpty()) {
- throw UploaderException("Found no git.properties files in $jarFile")
+ throw UploaderException(
+ "Found no git.properties files in $jarFile." +
+ " The 'artifactory-git-properties-jar' option must point to a JAR that contains" +
+ " a git.properties with the commit from which the JAR was built."
+ )
}
if (commitInfo.size > 1) {
throw UploaderException(
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/artifactory/ArtifactoryUploader.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/artifactory/ArtifactoryUploader.kt
index e03d098e4..ad792a2f5 100644
--- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/artifactory/ArtifactoryUploader.kt
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/artifactory/ArtifactoryUploader.kt
@@ -50,10 +50,12 @@ class ArtifactoryUploader(
FileWriter(uploadMetadataFile).use { writer ->
properties.store(writer, null)
}
- } catch (_: IOException) {
+ } catch (e: IOException) {
logger.warn(
- "Failed to create metadata file for automatic upload retry of {}. Please manually retry the coverage upload to Azure.",
- coverageFile
+ "Failed to create the retry metadata file for coverage upload {}." +
+ " The upload to Artifactory will not be retried automatically;" +
+ " you can re-upload the file manually.",
+ coverageFile, e
)
uploadMetadataFile.delete()
}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/azure/AzureFileStorageHttpUtils.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/azure/AzureFileStorageHttpUtils.kt
index d8b4041f3..c455beeed 100644
--- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/azure/AzureFileStorageHttpUtils.kt
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/azure/AzureFileStorageHttpUtils.kt
@@ -1,5 +1,6 @@
package com.teamscale.jacoco.agent.upload.azure
+import com.teamscale.client.BugReportMessages
import com.teamscale.jacoco.agent.upload.UploaderException
import com.teamscale.jacoco.agent.upload.azure.AzureHttpHeader.CONTENT_ENCODING
import com.teamscale.jacoco.agent.upload.azure.AzureHttpHeader.CONTENT_LANGUAGE
@@ -89,13 +90,25 @@ internal object AzureFileStorageHttpUtils {
val authKey = String(Base64.getEncoder().encode(mac.doFinal(stringToSign.toByteArray(charset("UTF-8")))))
return "SharedKey $account:$authKey"
} catch (e: NoSuchAlgorithmException) {
- throw UploaderException("Something is really wrong...", e)
+ throw UploaderException(
+ "Your JVM does not support the HMAC-SHA256 algorithm required for Azure shared-key authentication." +
+ " Please use a different JVM that includes support for it.", e
+ )
} catch (e: UnsupportedEncodingException) {
- throw UploaderException("Something is really wrong...", e)
+ throw UploaderException(
+ "The JVM does not support the UTF-8 charset required for Azure shared-key authentication." +
+ " Please use a different JVM that includes support for it.", e
+ )
} catch (e: InvalidKeyException) {
- throw UploaderException("The given access key is malformed: $key", e)
+ throw UploaderException(
+ "The Azure access key configured via the 'azure-key' option is malformed: ${e.message}." +
+ " The key must be a valid Base64 string copied from the Azure portal.", e
+ )
} catch (e: IllegalArgumentException) {
- throw UploaderException("The given access key is malformed: $key", e)
+ throw UploaderException(
+ "The Azure access key configured via the 'azure-key' option is not valid Base64: ${e.message}." +
+ " Copy the key as-is from the Azure portal.", e
+ )
}
}
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..e472cfab1 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
@@ -92,7 +92,7 @@ class AzureFileStorageUploader(
override fun uploadCoverageZip(coverageFile: File): Response {
val fileName = createFileName()
if (checkFile(fileName).isSuccessful) {
- logger.warn("The file $fileName does already exists at $uploadUrl")
+ logger.warn("A file named $fileName already exists at $uploadUrl. It will be overwritten.")
}
return createAndFillFile(coverageFile, fileName)
@@ -131,7 +131,12 @@ class AzureFileStorageUploader(
if (!checkDirectory(directoryPath).isSuccessful) {
val mkdirResponse = createDirectory(directoryPath)
if (!mkdirResponse.isSuccessful) {
- throw UploaderException("Creation of path '/$directoryPath' was unsuccessful", mkdirResponse)
+ throw UploaderException(
+ "Creating directory '$directoryPath' on the Azure file storage at $uploadUrl failed" +
+ " (HTTP ${mkdirResponse.code()} ${mkdirResponse.message()})." +
+ " Check the share name and that the configured access key has write permissions.",
+ mkdirResponse
+ )
}
}
}
@@ -199,7 +204,11 @@ class AzureFileStorageUploader(
if (response.isSuccessful) {
return fillFile(zipFile, fileName)
}
- logger.error("Creation of file '$fileName' was unsuccessful.")
+ logger.error(
+ "Creating file '$fileName' on the Azure file storage at $uploadUrl failed" +
+ " (HTTP ${response.code()} ${response.message()})." +
+ " Check 'azure-url' and 'azure-key' and that the share has free space."
+ )
return response
}
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..159ee21f9 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,5 +1,6 @@
package com.teamscale.jacoco.agent.upload.delay
+import com.teamscale.client.BugReportMessages
import com.teamscale.jacoco.agent.logging.LoggingUtils.getLogger
import com.teamscale.jacoco.agent.upload.IUploader
import com.teamscale.jacoco.agent.util.DaemonThreadFactory
@@ -81,7 +82,8 @@ class DelayedUploader internal constructor(
executor.execute { uploadCachedXmls() }
} else {
logger.error(
- "Tried to set upload commit multiple times (old uploader: {}, new commit: {}). This is a programming error. Please report a bug.",
+ "Tried to set the upload commit multiple times (old uploader: {}, new commit: {})." +
+ " ${BugReportMessages.REPORT_TO_CQSE}",
wrappedUploader?.describe(), information
)
}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/teamscale/TeamscaleConfig.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/teamscale/TeamscaleConfig.kt
index 76f031d14..753b921c6 100644
--- a/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/teamscale/TeamscaleConfig.kt
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/teamscale/TeamscaleConfig.kt
@@ -86,7 +86,10 @@ class TeamscaleConfig(
private fun parseCommit(commit: String): CommitDescriptor {
val split = commit.split(":".toRegex()).dropLastWhile { it.isEmpty() }
if (split.size != 2) {
- throw AgentOptionParseException("Invalid commit given $commit")
+ throw AgentOptionParseException(
+ "Invalid value '$commit' for option 'teamscale-commit'." +
+ " Expected format is 'branch:timestamp'."
+ )
}
return CommitDescriptor(split[0], split[1])
}
@@ -101,9 +104,15 @@ class TeamscaleConfig(
val branch = manifest.mainAttributes.getValue("Branch")
val timestamp = manifest.mainAttributes.getValue("Timestamp")
if (branch.isNullOrEmpty()) {
- throw AgentOptionParseException("No entry 'Branch' in MANIFEST")
+ throw AgentOptionParseException(
+ "No 'Branch' entry in MANIFEST.MF of $jarFile (configured via 'teamscale-commit-manifest-jar')." +
+ " Add 'Branch: ' and 'Timestamp: ' to the JAR's manifest."
+ )
} else if (timestamp.isNullOrEmpty()) {
- throw AgentOptionParseException("No entry 'Timestamp' in MANIFEST")
+ throw AgentOptionParseException(
+ "No 'Timestamp' entry in MANIFEST.MF of $jarFile (configured via 'teamscale-commit-manifest-jar')." +
+ " Add 'Timestamp: ' to the JAR's manifest."
+ )
}
logger.debug("Found commit $branch:$timestamp in file $jarFile")
return CommitDescriptor(branch, timestamp)
@@ -123,7 +132,11 @@ class TeamscaleConfig(
}
if (revision.isNullOrEmpty()) {
- throw AgentOptionParseException("No entry 'Revision' in MANIFEST")
+ throw AgentOptionParseException(
+ "No 'Revision' entry in MANIFEST.MF of $jarFile" +
+ " (configured via 'teamscale-revision-manifest-jar')." +
+ " Add 'Revision: ' to the JAR's manifest."
+ )
}
}
logger.debug("Found revision $revision in file $jarFile")
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..8136cd64a 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
@@ -131,7 +131,12 @@ class TeamscaleUploader(
)
return true
} catch (e: IOException) {
- logger.error("Failed to upload coverage to {}", teamscaleServer, e)
+ logger.error(
+ "Failed to upload coverage to Teamscale at {}." +
+ " The file is kept for an automatic retry on next agent startup." +
+ " If this persists, check network connectivity and the configured access token.",
+ teamscaleServer, e
+ )
return false
}
}
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 c61c2707d..2c707190c 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
@@ -1,5 +1,6 @@
package com.teamscale.jacoco.agent.util
+import com.teamscale.client.BugReportMessages
import com.teamscale.client.FileSystemUtils
import com.teamscale.client.TeamscaleServiceGenerator
import com.teamscale.jacoco.agent.PreMain
@@ -31,7 +32,12 @@ object AgentUtils {
"teamscale-java-profiler-${FileSystemUtils.toSafeFilename(ProcessInformationRetriever.pID)}-"
)
} catch (e: IOException) {
- throw RuntimeException("Failed to create temporary directory for agent files", e)
+ throw RuntimeException(
+ "Failed to create the agent's temporary directory under java.io.tmpdir" +
+ " (${System.getProperty("java.io.tmpdir")})." +
+ " Ensure the directory exists, is writable, and has free space.",
+ e
+ )
}
}
@@ -43,7 +49,11 @@ object AgentUtils {
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 agent directory. This is a bug, please report it.", e)
+ throw RuntimeException(
+ "Failed to obtain the agent's installation directory from its JAR URL." +
+ " ${BugReportMessages.REPORT_TO_CQSE}",
+ e
+ )
}
}
diff --git a/agent/src/test/kotlin/com/teamscale/jacoco/agent/options/TeamscalePropertiesUtilsTest.kt b/agent/src/test/kotlin/com/teamscale/jacoco/agent/options/TeamscalePropertiesUtilsTest.kt
index a63cf8bb2..d0767a6e8 100644
--- a/agent/src/test/kotlin/com/teamscale/jacoco/agent/options/TeamscalePropertiesUtilsTest.kt
+++ b/agent/src/test/kotlin/com/teamscale/jacoco/agent/options/TeamscalePropertiesUtilsTest.kt
@@ -50,7 +50,7 @@ internal class TeamscalePropertiesUtilsTest {
fun missingUsername() {
Files.write(teamscalePropertiesPath!!, "url=http://test\naccesskey=key".toByteArray(StandardCharsets.UTF_8))
Assertions.assertThatThrownBy { parseCredentials(teamscalePropertiesPath!!) }
- .hasMessageContaining("missing the username")
+ .hasMessageContaining("missing the 'username'")
}
@Test
@@ -58,7 +58,7 @@ internal class TeamscalePropertiesUtilsTest {
fun missingAccessKey() {
Files.write(teamscalePropertiesPath!!, "url=http://test\nusername=user".toByteArray(StandardCharsets.UTF_8))
Assertions.assertThatThrownBy { parseCredentials(teamscalePropertiesPath!!) }
- .hasMessageContaining("missing the accesskey")
+ .hasMessageContaining("missing the 'accesskey'")
}
@Test
@@ -66,7 +66,7 @@ internal class TeamscalePropertiesUtilsTest {
fun missingUrl() {
Files.write(teamscalePropertiesPath!!, "username=user\nusername=user".toByteArray(StandardCharsets.UTF_8))
Assertions.assertThatThrownBy { parseCredentials(teamscalePropertiesPath!!) }
- .hasMessageContaining("missing the url")
+ .hasMessageContaining("missing the 'url'")
}
@Test
diff --git a/agent/src/test/kotlin/com/teamscale/jacoco/agent/options/sapnwdi/SapNwdiApplicationTest.kt b/agent/src/test/kotlin/com/teamscale/jacoco/agent/options/sapnwdi/SapNwdiApplicationTest.kt
index 778aa0627..417af3e69 100644
--- a/agent/src/test/kotlin/com/teamscale/jacoco/agent/options/sapnwdi/SapNwdiApplicationTest.kt
+++ b/agent/src/test/kotlin/com/teamscale/jacoco/agent/options/sapnwdi/SapNwdiApplicationTest.kt
@@ -12,13 +12,19 @@ class SapNwdiApplicationTest {
Assertions.assertThatThrownBy { parseApplications("") }
.hasMessage("Application definition is expected not to be empty.")
Assertions.assertThatThrownBy { parseApplications(";") }
- .hasMessage("Application definition is expected not to be empty.")
+ .hasMessage(
+ "Empty entry in option 'sap-nwdi-applications'." +
+ " Provide entries in the form 'com.your.MarkerClass:teamscale-project-id', separated by ';'."
+ )
}
@Test
fun testIncompleteMarkerClassConfig() {
Assertions.assertThatThrownBy { parseApplications(":alias") }
- .hasMessage("Marker class is not given for :alias!")
+ .hasMessage(
+ "Option 'sap-nwdi-applications': no marker class given in entry ':alias'." +
+ " Use the form 'com.your.MarkerClass:teamscale-project-id'."
+ )
}
@Test
diff --git a/common-system-test/src/main/kotlin/com/teamscale/test/commons/TeamscaleMockServer.kt b/common-system-test/src/main/kotlin/com/teamscale/test/commons/TeamscaleMockServer.kt
index 89718d776..326500b78 100644
--- a/common-system-test/src/main/kotlin/com/teamscale/test/commons/TeamscaleMockServer.kt
+++ b/common-system-test/src/main/kotlin/com/teamscale/test/commons/TeamscaleMockServer.kt
@@ -290,7 +290,9 @@ class TeamscaleMockServer(val port: Int) {
val authHeader = request.headers("Authorization")
if (authHeader == null || authHeader != buildBasicAuthHeader(username, accessToken)) {
response.status(401)
- throw IllegalArgumentException("Unauthorized")
+ throw IllegalArgumentException(
+ "Unauthorized: the request was missing the expected Basic auth header for user '$username'."
+ )
}
}
}
diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/InternalImpactedTestEngine.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/InternalImpactedTestEngine.kt
index f5706f9cc..e1fb9665a 100644
--- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/InternalImpactedTestEngine.kt
+++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/InternalImpactedTestEngine.kt
@@ -91,7 +91,11 @@ internal class InternalImpactedTestEngine(
rootTestDescriptor.children.flatMap { engineTestDescriptor ->
val engineId = engineTestDescriptor.uniqueId.engineId
if (!engineId.isPresent) {
- LOG.severe { "Engine ID for test descriptor $engineTestDescriptor not present. Skipping execution of the engine." }
+ LOG.severe {
+ "A test engine returned a descriptor without an engine ID. Tests from this engine will be skipped." +
+ " Please report a bug to CQSE" +
+ " including the surrounding test descriptor: $engineTestDescriptor"
+ }
return@flatMap emptyList()
}
diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/AvailableTests.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/AvailableTests.kt
index e7b8fd285..5c3658192 100644
--- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/AvailableTests.kt
+++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/AvailableTests.kt
@@ -37,8 +37,11 @@ class AvailableTests {
fun convertToUniqueId(test: PrioritizableTest): Optional {
val clusterUniqueId = uniformPathToUniqueIdMapping[test.testName]
if (clusterUniqueId == null) {
- LOG.severe { "Retrieved invalid test '${test.testName}' from Teamscale server!" }
- LOG.severe { "The following seem related:" }
+ LOG.severe {
+ "Teamscale returned the impacted test '${test.testName}' which does not match any local test." +
+ " This test will be skipped. Please report a bug."
+ }
+ LOG.severe { "The following local tests seem related:" }
uniformPathToUniqueIdMapping.keys
.sortedBy { test.testName.levenshteinDistance(it) }
.take(5)
diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/ImpactedTestsProvider.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/ImpactedTestsProvider.kt
index 9dd41f11e..aa57a66e0 100644
--- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/ImpactedTestsProvider.kt
+++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/ImpactedTestsProvider.kt
@@ -55,9 +55,15 @@ open class ImpactedTestsProvider(
LOG.severe("""
Teamscale was not able to determine impacted tests:
${response.body()}
+ The test engine will fall back to executing all tests.
+ Verify the configured project, branch/timestamp, and partition exist in Teamscale.
""".trimIndent())
} else {
- LOG.severe("Retrieval of impacted tests failed: ${response.code()} ${response.message()}\n${getErrorBody(response)}")
+ LOG.severe(
+ "Retrieval of impacted tests failed: ${response.code()} ${response.message()}" +
+ "\n${getErrorBody(response)}" +
+ "\nThe test engine will fall back to executing all tests."
+ )
}
} catch (e: IOException) {
LOG.log(
@@ -86,7 +92,10 @@ open class ImpactedTestsProvider(
return true
} else {
LOG.severe {
- "Retrieved $returnedTests tests from Teamscale, but expected ${availableTestDetails.size}."
+ "Retrieved $returnedTests tests from Teamscale, but expected ${availableTestDetails.size}." +
+ " The test selection will fall back to executing all tests." +
+ " This usually indicates a mismatch between the local test set and what Teamscale knows" +
+ " — verify the project/branch/timestamp configuration."
}
return false
}
diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/ImpactedTestsSorter.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/ImpactedTestsSorter.kt
index 583d693cf..37b3812e7 100644
--- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/ImpactedTestsSorter.kt
+++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/ImpactedTestsSorter.kt
@@ -33,12 +33,20 @@ class ImpactedTestsSorter(private val impactedTestsProvider: ImpactedTestsProvid
allTests.forEach { test ->
val uniqueId = availableTests.convertToUniqueId(test)
if (!uniqueId.isPresent) {
- ImpactedTestEngine.LOG.severe { "Falling back to execute all..." }
+ ImpactedTestEngine.LOG.warning {
+ "Could not map impacted test '${test.testName}' (from Teamscale) to a local unique ID." +
+ " Falling back to executing all tests." +
+ " Check that the local test set matches the one indexed in Teamscale for this commit."
+ }
return
}
val availableTest = testDescriptor.findByUniqueId(uniqueId.get())
if (!availableTest.isPresent) {
- ImpactedTestEngine.LOG.severe { "Falling back to execute all..." }
+ ImpactedTestEngine.LOG.warning {
+ "Impacted test '${test.testName}' was mapped to a unique ID" +
+ " but no matching test descriptor exists in the current test hierarchy." +
+ " Falling back to executing all tests."
+ }
return
}
val descriptor = availableTest.get()
diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptionUtils.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptionUtils.kt
index 52c35315b..d82370931 100644
--- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptionUtils.kt
+++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptionUtils.kt
@@ -57,13 +57,22 @@ object TestEngineOptionUtils {
}
}
- private fun PrefixingPropertyReader.createServerOptions() =
- ServerOptions(
- getString("server.url") ?: throw AssertionError("server url is required"),
- getString("server.project") ?: throw AssertionError("project is required"),
- getString("server.userName") ?: throw AssertionError("username is required"),
- getString("server.userAccessToken") ?: throw AssertionError("access token is required")
- )
+ private fun PrefixingPropertyReader.createServerOptions(): ServerOptions {
+ val url = getString("server.url")
+ ?: throw IllegalArgumentException(missingServerOptionMessage("server.url"))
+ val project = getString("server.project")
+ ?: throw IllegalArgumentException(missingServerOptionMessage("server.project"))
+ val userName = getString("server.userName")
+ ?: throw IllegalArgumentException(missingServerOptionMessage("server.userName"))
+ val userAccessToken = getString("server.userAccessToken")
+ ?: throw IllegalArgumentException(missingServerOptionMessage("server.userAccessToken"))
+ return ServerOptions(url, project, userName, userAccessToken)
+ }
+
+ private fun missingServerOptionMessage(suffix: String) =
+ "Missing Teamscale option 'teamscale.test.impacted.$suffix'." +
+ " Set it via JUnit Platform configuration parameters" +
+ " (e.g. junit-platform.properties or via the build tool plugin)."
private class PrefixingPropertyReader(
private val prefix: String,
diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.kt
index 62b2ddd94..d9707ebd8 100644
--- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.kt
+++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.kt
@@ -16,7 +16,9 @@ class CucumberPickleDescriptorResolver : ITestDescriptorResolver {
LOG.fine { "Resolved feature: $featurePath" }
if (!featurePath.isPresent) {
LOG.severe {
- "Cannot resolve the feature classpath for ${descriptor}. This is probably a bug. Please report to CQSE"
+ "Cannot resolve the feature classpath for Cucumber test descriptor '${descriptor.displayName}'." +
+ " Please report a bug" +
+ " including the full descriptor: $descriptor"
}
return Optional.empty()
}
@@ -24,7 +26,9 @@ class CucumberPickleDescriptorResolver : ITestDescriptorResolver {
LOG.fine { "Resolved pickle name: $pickleName" }
if (!pickleName.isPresent) {
LOG.severe {
- "Cannot resolve the pickle name for ${descriptor}. This is probably a bug. Please report to CQSE"
+ "Cannot resolve the pickle name for Cucumber test descriptor '${descriptor.displayName}'." +
+ " Please report a bug to CQSE" +
+ " including the full descriptor: $descriptor"
}
return Optional.empty()
}
diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitClassBasedTestDescriptorResolverBase.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitClassBasedTestDescriptorResolverBase.kt
index 40e097ca6..9912bc17c 100644
--- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitClassBasedTestDescriptorResolverBase.kt
+++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitClassBasedTestDescriptorResolverBase.kt
@@ -21,7 +21,9 @@ abstract class JUnitClassBasedTestDescriptorResolverBase : ITestDescriptorResolv
if (!classSegmentName.isPresent) {
LOG.severe {
- "Falling back to unique ID as cluster id because class segment name could not be determined for test descriptor: $descriptor"
+ "Could not determine a class name for test descriptor '${descriptor.displayName}';" +
+ " using its unique ID as the impact-analysis cluster ID." +
+ " Tests in this descriptor may be over-selected for execution."
}
// Default to uniform path.
return Optional.of(descriptor.uniqueId.toString())
diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorUtils.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorUtils.kt
index fcf2e2ba6..70ee7909b 100644
--- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorUtils.kt
+++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorUtils.kt
@@ -102,7 +102,10 @@ object TestDescriptorUtils {
.forEach { testDescriptor ->
val engineId = testDescriptor.uniqueId.engineId
if (!engineId.isPresent) {
- LOG.severe { "Unable to determine engine ID for $testDescriptor!" }
+ LOG.severe {
+ "Could not determine the JUnit engine for test descriptor '${testDescriptor.displayName}'." +
+ " This test will not be considered for impact analysis."
+ }
return@forEach
}
@@ -111,12 +114,18 @@ object TestDescriptorUtils {
val uniformPath = testDescriptorResolver.getUniformPath(testDescriptor)
if (!uniformPath.isPresent) {
- LOG.severe { "Unable to determine uniform path for test descriptor: $testDescriptor" }
+ LOG.severe {
+ "Could not determine a uniform path for test descriptor '${testDescriptor.displayName}'." +
+ " This test will be skipped during impact analysis."
+ }
return@forEach
}
if (!clusterId.isPresent) {
- LOG.severe { "Unable to determine cluster id path for test descriptor: $testDescriptor" }
+ LOG.severe {
+ "Could not determine an impact-analysis cluster ID for test descriptor '${testDescriptor.displayName}'." +
+ " This test will be skipped during impact analysis."
+ }
return@forEach
}
diff --git a/installer/src/main/kotlin/com/teamscale/profiler/installer/steps/InstallAgentFilesStep.kt b/installer/src/main/kotlin/com/teamscale/profiler/installer/steps/InstallAgentFilesStep.kt
index 2f85a882c..03450ed16 100644
--- a/installer/src/main/kotlin/com/teamscale/profiler/installer/steps/InstallAgentFilesStep.kt
+++ b/installer/src/main/kotlin/com/teamscale/profiler/installer/steps/InstallAgentFilesStep.kt
@@ -68,7 +68,11 @@ class InstallAgentFilesStep(private val sourceDirectory: Path, private val insta
}
}
} catch (e: IOException) {
- throw PermissionError("Failed to list all files in $installDirectory.", e)
+ throw PermissionError(
+ "Failed to list all files in $installDirectory." +
+ " Check that the directory is readable and not concurrently modified.",
+ e
+ )
}
}
@@ -88,7 +92,11 @@ class InstallAgentFilesStep(private val sourceDirectory: Path, private val insta
properties.store(out, null)
}
} catch (e: IOException) {
- throw PermissionError("Failed to write $teamscalePropertiesPath.", e)
+ throw PermissionError(
+ "Failed to write $teamscalePropertiesPath." +
+ " Verify that the parent directory is writable and that the disk is not full.",
+ e
+ )
}
InstallFileUtils.makeReadable(teamscalePropertiesPath)
@@ -109,7 +117,11 @@ class InstallAgentFilesStep(private val sourceDirectory: Path, private val insta
@Throws(FatalInstallerError::class)
private fun createAgentDirectory() {
if (Files.exists(installDirectory)) {
- throw FatalInstallerError("Cannot install to $installDirectory: Path already exists")
+ throw FatalInstallerError(
+ "Cannot install to '$installDirectory': the path already exists." +
+ " Uninstall any previous installation first or choose a different target directory" +
+ " with --install-dir."
+ )
}
InstallFileUtils.createDirectory(installDirectory)
diff --git a/installer/src/main/kotlin/com/teamscale/profiler/installer/steps/InstallEtcEnvironmentStep.kt b/installer/src/main/kotlin/com/teamscale/profiler/installer/steps/InstallEtcEnvironmentStep.kt
index e507feb02..b8d7bb7aa 100644
--- a/installer/src/main/kotlin/com/teamscale/profiler/installer/steps/InstallEtcEnvironmentStep.kt
+++ b/installer/src/main/kotlin/com/teamscale/profiler/installer/steps/InstallEtcEnvironmentStep.kt
@@ -50,7 +50,11 @@ class InstallEtcEnvironmentStep(
}
} catch (e: IOException) {
- throw PermissionError("Failed to modify ${environmentFile.toAbsolutePath()}.", e)
+ throw PermissionError(
+ "Failed to modify ${environmentFile.toAbsolutePath()}." +
+ " Ensure the installer is running as root and that no other process is editing the file.",
+ e
+ )
}
}
diff --git a/installer/src/main/kotlin/com/teamscale/profiler/installer/steps/InstallSystemdStep.kt b/installer/src/main/kotlin/com/teamscale/profiler/installer/steps/InstallSystemdStep.kt
index d1e257835..ec670413e 100644
--- a/installer/src/main/kotlin/com/teamscale/profiler/installer/steps/InstallSystemdStep.kt
+++ b/installer/src/main/kotlin/com/teamscale/profiler/installer/steps/InstallSystemdStep.kt
@@ -32,7 +32,11 @@ class InstallSystemdStep(
try {
Files.createDirectories(systemdSystemConfDDirectory)
} catch (e: IOException) {
- throw PermissionError("Cannot create system.conf.d directory: $systemdSystemConfDDirectory", e)
+ throw PermissionError(
+ "Cannot create the systemd 'system.conf.d' directory at $systemdSystemConfDDirectory" +
+ " (${e.message}). Run the installer as root.",
+ e
+ )
}
}
@@ -53,7 +57,11 @@ class InstallSystemdStep(
try {
Files.writeString(systemdConfigFile, content)
} catch (e: IOException) {
- throw PermissionError("Could not create $systemdConfigFile", e)
+ throw PermissionError(
+ "Could not create the systemd configuration file at $systemdConfigFile (${e.message})." +
+ " Run the installer as root and ensure /etc/systemd/system.conf.d is writable.",
+ e
+ )
}
daemonReload()
diff --git a/installer/src/main/kotlin/com/teamscale/profiler/installer/utils/InstallFileUtils.kt b/installer/src/main/kotlin/com/teamscale/profiler/installer/utils/InstallFileUtils.kt
index 6536855c6..79b739418 100644
--- a/installer/src/main/kotlin/com/teamscale/profiler/installer/utils/InstallFileUtils.kt
+++ b/installer/src/main/kotlin/com/teamscale/profiler/installer/utils/InstallFileUtils.kt
@@ -23,7 +23,12 @@ object InstallFileUtils {
try {
Files.createDirectories(directory)
} catch (e: IOException) {
- throw PermissionError("Cannot create directory $directory", e)
+ throw PermissionError(
+ "Cannot create directory '$directory': ${e.message}." +
+ " Check that the parent directory exists and that you have write permissions" +
+ " (try running the installer as root/Administrator).",
+ e
+ )
}
}
}
diff --git a/installer/src/main/kotlin/com/teamscale/profiler/installer/utils/TeamscaleUtils.kt b/installer/src/main/kotlin/com/teamscale/profiler/installer/utils/TeamscaleUtils.kt
index c522cad2d..5e1d60a8b 100644
--- a/installer/src/main/kotlin/com/teamscale/profiler/installer/utils/TeamscaleUtils.kt
+++ b/installer/src/main/kotlin/com/teamscale/profiler/installer/utils/TeamscaleUtils.kt
@@ -90,7 +90,8 @@ object TeamscaleUtils {
if (!response.isSuccessful) {
throw FatalInstallerError(
- "Unexpected response from Teamscale, HTTP status ${response.code} ${response.message}"
+ "Unexpected response from Teamscale at ${credentials.url}," +
+ " HTTP ${response.code} ${response.message}: ${response.body.string()}."
)
}
}
diff --git a/installer/src/test/kotlin/com/teamscale/profiler/installer/AllPlatformsInstallerTest.kt b/installer/src/test/kotlin/com/teamscale/profiler/installer/AllPlatformsInstallerTest.kt
index 253bfb542..b97711e49 100644
--- a/installer/src/test/kotlin/com/teamscale/profiler/installer/AllPlatformsInstallerTest.kt
+++ b/installer/src/test/kotlin/com/teamscale/profiler/installer/AllPlatformsInstallerTest.kt
@@ -108,7 +108,7 @@ internal class AllPlatformsInstallerTest {
@Throws(IOException::class)
fun profilerAlreadyInstalled() {
Files.createDirectories(targetDirectory)
- Assertions.assertThatThrownBy { install() }.hasMessageContaining("Path already exists")
+ Assertions.assertThatThrownBy { install() }.hasMessageContaining("the path already exists")
}
@Test
diff --git a/report-generator/src/main/java/com/teamscale/jacoco/agent/options/ClasspathUtils.java b/report-generator/src/main/java/com/teamscale/jacoco/agent/options/ClasspathUtils.java
index 4d5862527..1691574de 100644
--- a/report-generator/src/main/java/com/teamscale/jacoco/agent/options/ClasspathUtils.java
+++ b/report-generator/src/main/java/com/teamscale/jacoco/agent/options/ClasspathUtils.java
@@ -34,8 +34,10 @@ private static List resolveClassPathEntries(String key, FilePatternResolve
try {
filePaths = FileSystemUtils.readLinesUTF8(txtFile);
} catch (IOException e) {
- throw new IOException("Failed read class path entries from the provided " + txtFile +
- " in the `" + key + "` option.", e);
+ throw new IOException(
+ "Failed to read class-path entries from " + txtFile + " (configured via option '" + key + "')."
+ + " Verify the file exists and is readable by the user running the JVM.",
+ e);
}
List resolvedFiles = new ArrayList<>();
for (String filePath : filePaths) {
diff --git a/report-generator/src/main/java/com/teamscale/jacoco/agent/options/FilePatternResolver.java b/report-generator/src/main/java/com/teamscale/jacoco/agent/options/FilePatternResolver.java
index 4e58f92b3..f0a9e27c4 100644
--- a/report-generator/src/main/java/com/teamscale/jacoco/agent/options/FilePatternResolver.java
+++ b/report-generator/src/main/java/com/teamscale/jacoco/agent/options/FilePatternResolver.java
@@ -67,7 +67,10 @@ public List resolveToMultipleFiles(String optionName, String pattern) thro
try {
return Collections.singletonList(workingDirectory.toPath().resolve(Paths.get(pattern)).toFile());
} catch (InvalidPathException e) {
- throw new IOException("Invalid path given for option " + optionName + ": " + pattern, e);
+ throw new IOException(
+ "Invalid path given for option '" + optionName + "': '" + pattern + "' (" + e.getMessage()
+ + "). Use a normal file system path or an Ant-style pattern.",
+ e);
}
}
@@ -86,7 +89,10 @@ public List resolveToMultipleFiles(String optionName, String pattern) thro
try {
return workingDirectory.toPath().resolve(Paths.get(pattern));
} catch (InvalidPathException e) {
- throw new IOException("Invalid path given for option " + optionName + ": " + pattern, e);
+ throw new IOException(
+ "Invalid path given for option '" + optionName + "': '" + pattern + "' (" + e.getMessage()
+ + "). Use a normal file system path or an Ant-style pattern.",
+ e);
}
}
@@ -193,8 +199,8 @@ private Path getSinglePath() throws IOException {
private List getAllMatchingPaths() {
if (this.matchingPaths.isEmpty()) {
logger.warn(
- "The pattern " + this.suffixPattern + " in " + this.basePath
- .toString() + " for option " + optionName + " did not match any file!");
+ "The pattern '" + this.suffixPattern + "' under '" + this.basePath
+ + "' (option '" + optionName + "') matched no files. Check the pattern and the working directory.");
}
logger.info("Resolved " + pattern + " to " + this.matchingPaths.size() + " for option " + optionName);
return this.matchingPaths;
diff --git a/report-generator/src/main/java/com/teamscale/report/jacoco/OpenAnalyzer.java b/report-generator/src/main/java/com/teamscale/report/jacoco/OpenAnalyzer.java
index a341522b5..d59e7d928 100644
--- a/report-generator/src/main/java/com/teamscale/report/jacoco/OpenAnalyzer.java
+++ b/report-generator/src/main/java/com/teamscale/report/jacoco/OpenAnalyzer.java
@@ -160,8 +160,12 @@ public void analyzeClass(final InputStream input, final String location)
protected IOException analyzerError(final String location,
final Exception cause) {
final IOException ex = new IOException(
- String.format("Error while analyzing %s with JaCoCo %s/%s.",
- location, JaCoCo.VERSION, JaCoCo.COMMITID_SHORT));
+ 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;
}
diff --git a/report-generator/src/main/java/org/jacoco/core/internal/analysis/CachingInstructionsBuilder.java b/report-generator/src/main/java/org/jacoco/core/internal/analysis/CachingInstructionsBuilder.java
index 0e07a6f2d..b4cf61e14 100644
--- a/report-generator/src/main/java/org/jacoco/core/internal/analysis/CachingInstructionsBuilder.java
+++ b/report-generator/src/main/java/org/jacoco/core/internal/analysis/CachingInstructionsBuilder.java
@@ -196,7 +196,11 @@ private Instruction getPredecessor(Instruction instruction) {
instruction = (Instruction) predecessorField.get(instruction);
} catch (NoSuchFieldException | IllegalAccessException e) {
// This means we have a serious coding mistake here there is no way to recover from this anyway
- throw new RuntimeException("Instruction has no field named predecessor! This is a programming error!", e);
+ throw new RuntimeException(
+ "Reflection failed to access internal Instruction.predecessor field."
+ + " This usually means the bundled JaCoCo version was upgraded without updating this class."
+ + " Please report a bug.",
+ e);
}
return instruction;
}
diff --git a/report-generator/src/main/kotlin/com/teamscale/report/ReportUtils.kt b/report-generator/src/main/kotlin/com/teamscale/report/ReportUtils.kt
index 40892a4e7..dcda59d42 100644
--- a/report-generator/src/main/kotlin/com/teamscale/report/ReportUtils.kt
+++ b/report-generator/src/main/kotlin/com/teamscale/report/ReportUtils.kt
@@ -40,7 +40,10 @@ object ReportUtils {
private fun writeReportToFile(reportFile: File, report: T) {
val directory = reportFile.getParentFile()
if (!directory.isDirectory() && !directory.mkdirs()) {
- throw IOException("Failed to create directory " + directory.absolutePath)
+ throw IOException(
+ "Failed to create the report output directory '${directory.absolutePath}'." +
+ " Verify the parent directory exists and is writable."
+ )
}
JsonUtils.serializeToFile(reportFile, report)
}
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 6bd6df031..0d968b445 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
@@ -43,7 +43,12 @@ open class FilteringAnalyzer(
analyzeClass(buffer)
} catch (cause: RuntimeException) {
if (cause.isUnsupportedClassFile()) {
- logger.error(cause.message + " in " + location)
+ 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" +
+ " — exclude it via 'excludes' or upgrade the profiler.",
+ cause
+ )
} else {
throw analyzerError(location, cause)
}
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 f6beb165a..5ce800429 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,7 +155,10 @@ abstract class JaCoCoBasedReportGenerator(
return
}
- EDuplicateClassFileBehavior.FAIL -> error { "Can't add different class with same name: ${coverage.name}" }
+ 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/TestwiseCoverageReportWriter.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/TestwiseCoverageReportWriter.kt
index 5e6df2ff5..cb71859d7 100644
--- a/report-generator/src/main/kotlin/com/teamscale/report/testwise/TestwiseCoverageReportWriter.kt
+++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/TestwiseCoverageReportWriter.kt
@@ -43,7 +43,11 @@ class TestwiseCoverageReportWriter(
writeTestInfo(testInfo)
} catch (e: IOException) {
// Need to be wrapped in RuntimeException as Consumer does not allow to throw a checked Exception
- throw RuntimeException("Writing test info to report failed.", e)
+ throw RuntimeException(
+ "Writing test info for '${testInfo.uniformPath}' to the testwise report failed: ${e.message}." +
+ " Check disk space and that the agent's 'out' directory is writable.",
+ e
+ )
}
}
diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/CachingExecutionDataReader.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/CachingExecutionDataReader.kt
index a4609b9d6..d189fd845 100644
--- a/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/CachingExecutionDataReader.kt
+++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/CachingExecutionDataReader.kt
@@ -29,7 +29,9 @@ open class CachingExecutionDataReader(
*/
fun analyzeClassDirs() {
if (classesDirectories.isEmpty()) {
- logger.warn("No class directories found for caching.")
+ logger.warn(
+ "No class directories provided — testwise coverage will be calculated with out caching."
+ )
return
}
val analyzer = AnalyzerCache(probeCache, locationIncludeFilter, logger)
@@ -68,7 +70,10 @@ open class CachingExecutionDataReader(
private fun validateAnalysisResult(classCount: Int) {
val directoryList = classesDirectories.joinToString(",") { it.path }
when {
- classCount == 0 -> logger.error("No class files found in directories: $directoryList")
+ classCount == 0 -> logger.error(
+ "No class files found in directories: $directoryList." +
+ " Verify that 'class-dir' points to compiled output and not the source tree."
+ )
probeCache.isEmpty -> logger.error(
"None of the $classCount class files found in the given directories match the configured include/exclude patterns! $directoryList"
)
@@ -93,7 +98,13 @@ open class CachingExecutionDataReader(
)
runCatching { buildCoverage(testId, dump.store, locationIncludeFilter) }
.onSuccess(nextConsumer::accept)
- .onFailure { e -> logger.error("Failed to generate coverage for test $testId", e) }
+ .onFailure { e ->
+ logger.error(
+ "Failed to generate coverage for test $testId." +
+ " This test's coverage will be missing from the report. Check the agent log for the cause.",
+ e
+ )
+ }
}
/**
diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/BugReportMessages.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/BugReportMessages.kt
new file mode 100644
index 000000000..422341f9c
--- /dev/null
+++ b/teamscale-client/src/main/kotlin/com/teamscale/client/BugReportMessages.kt
@@ -0,0 +1,8 @@
+package com.teamscale.client
+
+/** Standard wording for messages that ask users to report an internal error to CQSE. */
+object BugReportMessages {
+ /** Standard sentence to append to messages reporting an internal error. */
+ const val REPORT_TO_CQSE: String =
+ "This is an internal error in the Teamscale Java Profiler. Please report a bug to CQSE."
+}
diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/ConnectExceptionRetryInterceptor.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/ConnectExceptionRetryInterceptor.kt
index ad7c586d2..4870db3e7 100644
--- a/teamscale-client/src/main/kotlin/com/teamscale/client/ConnectExceptionRetryInterceptor.kt
+++ b/teamscale-client/src/main/kotlin/com/teamscale/client/ConnectExceptionRetryInterceptor.kt
@@ -56,7 +56,10 @@ class ConnectExceptionRetryInterceptor(private val timeout: Duration) : Intercep
}
// If we reach here, we've timed out without a successful response
- throw lastException ?: IOException("Failed to execute request after 1 minute of retries")
+ throw lastException ?: IOException(
+ "Failed to execute request to Teamscale after $timeoutMillis ms of connect retries." +
+ " Verify network connectivity and that the Teamscale host is reachable."
+ )
}
/**
diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/FileSystemUtils.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/FileSystemUtils.kt
index fe484f970..c04a0da6c 100644
--- a/teamscale-client/src/main/kotlin/com/teamscale/client/FileSystemUtils.kt
+++ b/teamscale-client/src/main/kotlin/com/teamscale/client/FileSystemUtils.kt
@@ -117,7 +117,10 @@ object FileSystemUtils {
@Throws(IOException::class)
fun ensureDirectoryExists(directory: File) {
if (!directory.exists() && !directory.mkdirs()) {
- throw IOException("Couldn't create directory: $directory")
+ throw IOException(
+ "Could not create directory '$directory'." +
+ " Verify that the parent directory is writable."
+ )
}
if (directory.exists() && directory.canWrite()) {
return
@@ -135,10 +138,16 @@ object FileSystemUtils {
}
}
if (!directory.exists()) {
- throw IOException("Temp directory $directory could not be created.")
+ throw IOException(
+ "Temp directory $directory could not be created." +
+ " Verify java.io.tmpdir is set to a writable location."
+ )
}
if (!directory.canWrite()) {
- throw IOException("Temp directory $directory exists, but is not writable.")
+ throw IOException(
+ "Temp directory $directory exists, but is not writable." +
+ " Set java.io.tmpdir to a writable location or grant write permissions to the current user."
+ )
}
}
diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/HttpUtils.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/HttpUtils.kt
index 906196f79..8f7fbd177 100644
--- a/teamscale-client/src/main/kotlin/com/teamscale/client/HttpUtils.kt
+++ b/teamscale-client/src/main/kotlin/com/teamscale/client/HttpUtils.kt
@@ -101,7 +101,11 @@ object HttpUtils {
val host = proxySystemProperties.proxyHost ?: return false
useProxyServer(httpClientBuilder, host, proxySystemProperties.proxyPort)
} catch (e: ProxySystemProperties.IncorrectPortFormatException) {
- LOGGER.warn(e.message)
+ LOGGER.warn(
+ "Ignoring invalid proxy port from system properties (http.proxyPort/https.proxyPort): {}." +
+ " The proxy will not be used until a valid port is configured.",
+ e.message
+ )
return false
}
diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/ITeamscaleService.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/ITeamscaleService.kt
index c7e52e018..52f2ccac0 100644
--- a/teamscale-client/src/main/kotlin/com/teamscale/client/ITeamscaleService.kt
+++ b/teamscale-client/src/main/kotlin/com/teamscale/client/ITeamscaleService.kt
@@ -196,8 +196,15 @@ fun ITeamscaleService.uploadReport(
}
val errorBody = HttpUtils.getErrorBodyStringSafe(response)
- throw IOException("Request failed with error code ${response.code()}. Response body: $errorBody")
+ throw IOException(
+ "Uploading the report to Teamscale failed with HTTP ${response.code()}: $errorBody." +
+ " Check the partition, project, branch/timestamp, and the user's permissions in Teamscale."
+ )
} catch (e: IOException) {
- throw IOException("Failed to upload report. ${e.message}", e)
+ throw IOException(
+ "Failed to upload report to Teamscale: ${e.message}." +
+ " Verify network connectivity and the configured Teamscale URL and credentials.",
+ e
+ )
}
}
diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleClient.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleClient.kt
index c48288d60..507a34814 100644
--- a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleClient.kt
+++ b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleClient.kt
@@ -31,7 +31,9 @@ open class TeamscaleClient {
readTimeout: Duration = HttpUtils.DEFAULT_READ_TIMEOUT,
writeTimeout: Duration = HttpUtils.DEFAULT_WRITE_TIMEOUT
) {
- val url = baseUrl?.toHttpUrlOrNull() ?: throw IllegalArgumentException("Invalid URL: $baseUrl")
+ val url = baseUrl?.toHttpUrlOrNull() ?: throw IllegalArgumentException(
+ "Invalid Teamscale base URL: '$baseUrl'. Use the form 'https://teamscale.example.com/'."
+ )
this.projectId = projectId
service = TeamscaleServiceGenerator.createService(
url, user, accessToken, userAgent, readTimeout, writeTimeout
@@ -50,7 +52,9 @@ open class TeamscaleClient {
writeTimeout: Duration = HttpUtils.DEFAULT_WRITE_TIMEOUT,
userAgent: String
) {
- val url = baseUrl?.toHttpUrlOrNull() ?: throw IllegalArgumentException("Invalid URL: $baseUrl")
+ val url = baseUrl?.toHttpUrlOrNull() ?: throw IllegalArgumentException(
+ "Invalid Teamscale base URL: '$baseUrl'. Use the form 'https://teamscale.example.com/'."
+ )
this.projectId = projectId
service = TeamscaleServiceGenerator.createServiceWithRequestLogging(
url, user, accessToken, logfile, readTimeout, writeTimeout, userAgent
@@ -229,7 +233,10 @@ open class TeamscaleClient {
val sessionId =
service.createSession(projectId, commitDescriptor, revision, repository, partition, message)
.executeOrThrow()
- require(sessionId != null) { "Session ID was null" }
+ require(sessionId != null) {
+ "Teamscale did not return a session ID for the upload." +
+ " The server may be misconfigured — check the Teamscale logs."
+ }
for ((reportFormat, files) in reports) {
val partList = files.map { file ->
@@ -299,7 +306,10 @@ open class TeamscaleClient {
fun Call.executeOrThrow(): T? {
val response = execute()
if (!response.isSuccessful) {
- throw IOException("HTTP request " + request() + " failed: " + HttpUtils.getErrorBodyStringSafe(response))
+ throw IOException(
+ "Request to Teamscale failed: HTTP ${response.code()} ${response.message()} for " +
+ "${request().method} ${request().url}. Response body: ${HttpUtils.getErrorBodyStringSafe(response)}"
+ )
}
return response.body()
}
diff --git a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TeamscaleUpload.kt b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TeamscaleUpload.kt
index accbab7bf..ede20e2c2 100755
--- a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TeamscaleUpload.kt
+++ b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TeamscaleUpload.kt
@@ -162,8 +162,10 @@ abstract class TeamscaleUpload : DefaultTask() {
server.toClient().uploadReports(reports)
} catch (e: Exception) {
if (ignoreFailures.get()) {
- logger.warn("Ignoring failure during upload:")
- logger.warn(e.message, e)
+ logger.warn(
+ "Ignoring failure during upload to Teamscale at {} (ignoreFailures=true): {}",
+ server.url.get(), e.message, e
+ )
} else {
throw e
}
@@ -207,7 +209,12 @@ abstract class TeamscaleUpload : DefaultTask() {
)
}
} catch (e: IOException) {
- throw GradleException("Upload failed (${e.message})", e)
+ throw GradleException(
+ "Uploading reports to Teamscale failed: ${e.message}." +
+ " Verify network connectivity, credentials," +
+ " and that the project/branch/timestamp exist in Teamscale.",
+ e
+ )
}
}
diff --git a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/ServerConfiguration.kt b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/ServerConfiguration.kt
index 090befee0..7e7a8a9a4 100644
--- a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/ServerConfiguration.kt
+++ b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/ServerConfiguration.kt
@@ -22,19 +22,23 @@ abstract class ServerConfiguration : Serializable {
fun validate() {
if (url.get().isBlank()) {
- throw GradleException("Teamscale server url must not be empty!")
+ throw GradleException(missingServerPropertyMessage("url", "https://teamscale.example.com/"))
}
if (project.get().isBlank()) {
- throw GradleException("Teamscale project name must not be empty!")
+ throw GradleException(missingServerPropertyMessage("project", "my-project-id"))
}
if (userName.get().isBlank()) {
- throw GradleException("Teamscale user name must not be empty!")
+ throw GradleException(missingServerPropertyMessage("userName", "alice"))
}
if (userAccessToken.get().isBlank()) {
- throw GradleException("Teamscale user access token must not be empty!")
+ throw GradleException(missingServerPropertyMessage("userAccessToken", ""))
}
}
+ private fun missingServerPropertyMessage(property: String, example: String) =
+ "Teamscale server '$property' must not be empty." +
+ " Set it via 'teamscale { server { $property.set(\"$example\") } }' in your build.gradle.kts."
+
fun toClient() = TeamscaleClient(
url.get(), userName.get(), userAccessToken.get(), project.get(),
userAgent = TeamscaleServiceGenerator.buildUserAgent("Teamscale Gradle Plugin", BuildVersion.pluginVersion)
diff --git a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/reporting/compact/CompactCoverageReport.kt b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/reporting/compact/CompactCoverageReport.kt
index 36158594a..9177be8c0 100644
--- a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/reporting/compact/CompactCoverageReport.kt
+++ b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/reporting/compact/CompactCoverageReport.kt
@@ -55,7 +55,12 @@ abstract class CompactCoverageReport : JaCoCoBasedReportTaskBase or in the plugin configuration,"
+ + " or run Maven from inside a Git checkout.");
}
Repository repository;
try (Git git = Git.open(gitDirectory.toFile())) {
diff --git a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TestwiseCoverageReportMojo.java b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TestwiseCoverageReportMojo.java
index f43dcd9ad..dae1aae67 100644
--- a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TestwiseCoverageReportMojo.java
+++ b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TestwiseCoverageReportMojo.java
@@ -118,8 +118,9 @@ private void generateTestwiseCoverageReport(JaCoCoTestwiseReportGenerator genera
try {
Files.createDirectories(reportsFolder);
} catch (IOException e) {
- logger.error("Could not create folder " + reportsFolder + ". Aborting.", e);
- throw new MojoFailureException(e);
+ throw new MojoFailureException(
+ "Could not create the testwise-coverage report folder " + reportsFolder
+ + ". Check that the parent directory is writable.", e);
}
List jacocoExecutionDataList = ReportUtils.listFiles(ETestArtifactFormat.JACOCO, reportFileDirectories);
String reportFilePath = reportsFolder.resolve("testwise-coverage.json").toString();
@@ -132,8 +133,9 @@ private void generateTestwiseCoverageReport(JaCoCoTestwiseReportGenerator genera
generator.convertAndConsume(executionDataFile, coverageWriter);
}
} catch (IOException e) {
- logger.error("Could not create testwise report. Aborting.", e);
- throw new MojoFailureException(e);
+ throw new MojoFailureException(
+ "Could not write the testwise coverage report to " + reportFilePath
+ + ". Check disk space and that the file is not held open by another process.", e);
}
}
@@ -153,8 +155,9 @@ private TestInfoFactory createTestInfoFactory(List reportFiles) throws Moj
logger.info("Writing report with " + testDetails.size() + " Details/" + testExecutions.size() + " Results");
return new TestInfoFactory(testDetails, testExecutions);
} catch (IOException e) {
- logger.error("Could not read test details from reports. Aborting.", e);
- throw new MojoFailureException(e);
+ throw new MojoFailureException(
+ "Could not read test details from JaCoCo report directories " + reportFiles
+ + ". Check that the files exist and are readable.", e);
}
}
diff --git a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaMojoBase.java b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaMojoBase.java
index c18bd5e1a..1918d71b7 100644
--- a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaMojoBase.java
+++ b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaMojoBase.java
@@ -233,15 +233,15 @@ private void setTiaProperties() {
* Automatically find an available port.
*/
private String findAvailablePort() {
- try (ServerSocket socket = new ServerSocket(0)) {
- int port = socket.getLocalPort();
- getLog().info("Automatically set server port to " + port);
- return String.valueOf(port);
- } catch (IOException e) {
- getLog().error("Port blocked, trying again.", e);
- return findAvailablePort();
+ try (ServerSocket socket = new ServerSocket(0)) {
+ int port = socket.getLocalPort();
+ getLog().info("Automatically set server port to " + port);
+ return String.valueOf(port);
+ } catch (IOException e) {
+ getLog().warn("Could not allocate a free port for the TIA agent, retrying.", e);
+ return findAvailablePort();
+ }
}
- }
/**
* Sets the teamscale-test-impacted engine as only includedEngine and passes all previous engine configuration to
@@ -358,7 +358,10 @@ private void createTargetDirectory() throws MojoFailureException {
}
Files.createDirectories(targetDirectory);
} catch (IOException e) {
- throw new MojoFailureException("Could not create target directory " + targetDirectory, e);
+ throw new MojoFailureException(
+ "Could not create target directory " + targetDirectory + " (" + e.getMessage() + ")."
+ + " Check that the parent directory is writable and there is enough disk space.",
+ e);
}
}
diff --git a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/upload/CoverageUploadMojo.java b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/upload/CoverageUploadMojo.java
index 7a5b2a221..d1f5d292f 100644
--- a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/upload/CoverageUploadMojo.java
+++ b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/upload/CoverageUploadMojo.java
@@ -241,7 +241,10 @@ private void insertReports(Map> reportsByFormat, EReportForma
continue;
}
if (!report.canRead()) {
- getLog().warn(String.format("Cannot read %s, skipping!", report.getAbsolutePath()));
+ getLog().warn(String.format(
+ "Cannot read coverage report '%s' (permission denied or file vanished). It will be skipped"
+ + " — check that the file exists and is readable by the Maven user.",
+ report.getAbsolutePath()));
continue;
}
reports.add(report);
diff --git a/tia-client/src/main/kotlin/com/teamscale/tia/client/CommandLineInterface.kt b/tia-client/src/main/kotlin/com/teamscale/tia/client/CommandLineInterface.kt
index 139b0ceb1..6753c1364 100644
--- a/tia-client/src/main/kotlin/com/teamscale/tia/client/CommandLineInterface.kt
+++ b/tia-client/src/main/kotlin/com/teamscale/tia/client/CommandLineInterface.kt
@@ -29,7 +29,8 @@ class CommandLineInterface(arguments: Array) {
init {
if (arguments.size < 2) {
throw InvalidCommandLineException(
- "You must provide at least two arguments: the agent's URL and the command to execute"
+ "You must provide at least two arguments: the agent's URL and a command." +
+ " Usage: [options]."
)
}
diff --git a/tia-client/src/main/kotlin/com/teamscale/tia/client/RunningTest.kt b/tia-client/src/main/kotlin/com/teamscale/tia/client/RunningTest.kt
index 88c64afde..479e20d9b 100644
--- a/tia-client/src/main/kotlin/com/teamscale/tia/client/RunningTest.kt
+++ b/tia-client/src/main/kotlin/com/teamscale/tia/client/RunningTest.kt
@@ -60,7 +60,11 @@ class RunningTest(private val uniformPath: String, private val api: ITestwiseCov
try {
return body.string()
} catch (e: IOException) {
- throw AgentHttpRequestFailedException("Unable to read agent HTTP response body string", e)
+ throw AgentHttpRequestFailedException(
+ "Unable to read the response body returned by the Teamscale Java profiler (HTTP I/O error)." +
+ " The profiler may have closed the connection — check its log for errors.",
+ e
+ )
}
}