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 + ) } }