stream = Files.list(directory)) {
- if (stream.findFirst().isPresent()) {
- return;
- }
- }
-
- Files.delete(directory);
- }
-
- /**
- * Dumps the current execution data, converts it, writes it to the output directory defined in {@link #options} and
- * uploads it if an uploader is configured. Logs any errors, never throws an exception.
- */
- @Override
- public void dumpReport() {
- logger.debug("Starting dump");
-
- try {
- dumpReportUnsafe();
- } catch (Throwable t) {
- // we want to catch anything in order to avoid crashing the whole system under
- // test
- logger.error("Dump job failed with an exception", t);
- }
- }
-
- private void dumpReportUnsafe() {
- Dump dump;
- try {
- dump = controller.dumpAndReset();
- } catch (JacocoRuntimeController.DumpException e) {
- logger.error("Dumping failed, retrying later", e);
- return;
- }
-
- try (Benchmark ignored = new Benchmark("Generating the XML report")) {
- File outputFile = options.createNewFileInOutputDirectory("jacoco", "xml");
- CoverageFile coverageFile = generator.convertSingleDumpToReport(dump, outputFile);
- uploader.upload(coverageFile);
- } catch (IOException e) {
- logger.error("Converting binary dump to XML failed", e);
- } catch (EmptyReportException e) {
- logger.error("No coverage was collected. " + e.getMessage(), e);
- }
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java b/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java
deleted file mode 100644
index 13b98f37e..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java
+++ /dev/null
@@ -1,157 +0,0 @@
-package com.teamscale.jacoco.agent;
-
-import com.teamscale.jacoco.agent.logging.LoggingUtils;
-import com.teamscale.jacoco.agent.options.AgentOptions;
-import org.eclipse.jetty.server.Server;
-import org.eclipse.jetty.server.ServerConnector;
-import org.eclipse.jetty.servlet.ServletContextHandler;
-import org.eclipse.jetty.servlet.ServletHolder;
-import org.eclipse.jetty.util.thread.QueuedThreadPool;
-import org.glassfish.jersey.server.ResourceConfig;
-import org.glassfish.jersey.servlet.ServletContainer;
-import org.jacoco.agent.rt.RT;
-import org.slf4j.Logger;
-
-import java.lang.management.ManagementFactory;
-
-/**
- * Base class for agent implementations. Handles logger shutdown, store creation and instantiation of the
- * {@link JacocoRuntimeController}.
- *
- * Subclasses must handle dumping onto disk and uploading via the configured uploader.
- */
-public abstract class AgentBase {
-
- /** The logger. */
- protected final Logger logger = LoggingUtils.getLogger(this);
-
- /** Controls the JaCoCo runtime. */
- public final JacocoRuntimeController controller;
-
- /** The agent options. */
- protected AgentOptions options;
-
- private Server server;
-
- /** Constructor. */
- public AgentBase(AgentOptions options) throws IllegalStateException {
- this.options = options;
-
- try {
- controller = new JacocoRuntimeController(RT.getAgent());
- } catch (IllegalStateException e) {
- throw new IllegalStateException(
- "Teamscale Java Profiler not started or there is a conflict with another agent on the classpath.",
- e);
- }
- logger.info("Starting Teamscale Java Profiler for process {} with options: {}",
- ManagementFactory.getRuntimeMXBean().getName(), getOptionsObjectToLog());
- if (options.getHttpServerPort() != null) {
- try {
- initServer();
- } catch (Exception e) {
- logger.error("Could not start http server on port " + options.getHttpServerPort()
- + ". Please check if the port is blocked.");
- throw new IllegalStateException("Control server not started.", e);
- }
- }
- }
-
-
-
- /**
- * Lazily generated string representation of the command line arguments to print to the log.
- */
- private Object getOptionsObjectToLog() {
- return new Object() {
- @Override
- public String toString() {
- if (options.shouldObfuscateSecurityRelatedOutputs()) {
- return options.getObfuscatedOptionsString();
- }
- return options.getOriginalOptionsString();
- }
- };
- }
-
- /**
- * Starts the http server, which waits for information about started and finished tests.
- */
- private void initServer() throws Exception {
- logger.info("Listening for test events on port {}.", options.getHttpServerPort());
-
- // Jersey Implementation
- ServletContextHandler handler = buildUsingResourceConfig();
- QueuedThreadPool threadPool = new QueuedThreadPool();
- threadPool.setMaxThreads(10);
- threadPool.setDaemon(true);
-
- // Create a server instance and set the thread pool
- server = new Server(threadPool);
- // Create a server connector, set the port and add it to the server
- ServerConnector connector = new ServerConnector(server);
- connector.setPort(options.getHttpServerPort());
- server.addConnector(connector);
- server.setHandler(handler);
- server.start();
- }
-
- private ServletContextHandler buildUsingResourceConfig() {
- ServletContextHandler handler = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
- handler.setContextPath("/");
-
- ResourceConfig resourceConfig = initResourceConfig();
- handler.addServlet(new ServletHolder(new ServletContainer(resourceConfig)), "/*");
- return handler;
- }
-
- /**
- * Initializes the {@link ResourceConfig} needed for the Jetty + Jersey Server
- */
- protected abstract ResourceConfig initResourceConfig();
-
- /**
- * Registers a shutdown hook that stops the timer and dumps coverage a final time.
- */
- void registerShutdownHook() {
- Runtime.getRuntime().addShutdownHook(new Thread(() -> {
- try {
- logger.info("Teamscale Java Profiler is shutting down...");
- stopServer();
- prepareShutdown();
- logger.info("Teamscale Java Profiler successfully shut down.");
- } catch (Exception e) {
- logger.error("Exception during profiler shutdown.", e);
- } finally {
- // Try to flush logging resources also in case of an exception during shutdown
- PreMain.closeLoggingResources();
- }
- }));
- }
-
- /** Stop the http server if it's running */
- void stopServer() {
- if (options.getHttpServerPort() != null) {
- try {
- server.stop();
- } catch (Exception e) {
- logger.error("Could not stop server so it is killed now.", e);
- } finally {
- server.destroy();
- }
- }
- }
-
- /** Called when the shutdown hook is triggered. */
- protected void prepareShutdown() {
- // Template method to be overridden by subclasses.
- }
-
- /**
- * Dumps the current execution data, converts it, writes it to the output
- * directory defined in {@link #options} and uploads it if an uploader is
- * configured. Logs any errors, never throws an exception.
- */
- public abstract void dumpReport();
-
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/AgentResource.java b/agent/src/main/java/com/teamscale/jacoco/agent/AgentResource.java
deleted file mode 100644
index 51684e739..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/AgentResource.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package com.teamscale.jacoco.agent;
-
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.core.Response;
-
-/**
- * The resource of the Jersey + Jetty http server holding all the endpoints specific for the {@link Agent}.
- */
-@Path("/")
-public class AgentResource extends ResourceBase {
-
- private static Agent agent;
-
- /**
- * Static setter to inject the {@link Agent} to the resource.
- */
- public static void setAgent(Agent agent) {
- AgentResource.agent = agent;
- ResourceBase.agentBase = agent;
- }
-
- /** Handles dumping a XML coverage report for coverage collected until now. */
- @POST
- @Path("/dump")
- public Response handleDump() {
- logger.debug("Dumping report triggered via HTTP request");
- agent.dumpReport();
- return Response.noContent().build();
- }
-
- /** Handles resetting of coverage. */
- @POST
- @Path("/reset")
- public Response handleReset() {
- logger.debug("Resetting coverage triggered via HTTP request");
- agent.controller.reset();
- return Response.noContent().build();
- }
-
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/DelayedLogger.java b/agent/src/main/java/com/teamscale/jacoco/agent/DelayedLogger.java
deleted file mode 100644
index 574389c58..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/DelayedLogger.java
+++ /dev/null
@@ -1,70 +0,0 @@
-package com.teamscale.jacoco.agent;
-
-import com.teamscale.report.util.ILogger;
-import org.slf4j.Logger;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A logger that buffers logs in memory and writes them to the actual logger at a later point. This is needed when stuff
- * needs to be logged before the actual logging framework is initialized.
- */
-public class DelayedLogger implements ILogger {
-
- /** List of log actions that will be executed once the logger is initialized. */
- private final List logActions = new ArrayList<>();
-
- @Override
- public void debug(String message) {
- logActions.add(logger -> logger.debug(message));
- }
-
- @Override
- public void info(String message) {
- logActions.add(logger -> logger.info(message));
- }
-
- @Override
- public void warn(String message) {
- logActions.add(logger -> logger.warn(message));
- }
-
- @Override
- public void warn(String message, Throwable throwable) {
- logActions.add(logger -> logger.warn(message, throwable));
- }
-
- @Override
- public void error(Throwable throwable) {
- logActions.add(logger -> logger.error(throwable.getMessage(), throwable));
- }
-
- @Override
- public void error(String message, Throwable throwable) {
- logActions.add(logger -> logger.error(message, throwable));
- }
-
- /**
- * Logs an error and also writes the message to {@link System#err} to ensure the message is even logged in case
- * setting up the logger itself fails for some reason (see TS-23151).
- */
- public void errorAndStdErr(String message, Throwable throwable) {
- System.err.println(message);
- logActions.add(logger -> logger.error(message, throwable));
- }
-
- /** Writes the logs to the given slf4j logger. */
- public void logTo(Logger logger) {
- logActions.forEach(action -> action.log(logger));
- }
-
- /** An action to be executed on a logger. */
- private interface ILoggerAction {
-
- /** Executes the action on the given logger. */
- void log(Logger logger);
-
- }
-}
-
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/GenericExceptionMapper.java b/agent/src/main/java/com/teamscale/jacoco/agent/GenericExceptionMapper.java
deleted file mode 100644
index 76c1d043f..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/GenericExceptionMapper.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.teamscale.jacoco.agent;
-
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.ext.ExceptionMapper;
-
-/**
- * Generates a {@link Response} for an exception.
- */
-@javax.ws.rs.ext.Provider
-public class GenericExceptionMapper implements ExceptionMapper {
-
- @Override
- public Response toResponse(Throwable e) {
- Response.ResponseBuilder errorResponse = Response.status(Response.Status.INTERNAL_SERVER_ERROR);
- errorResponse.type(MediaType.TEXT_PLAIN_TYPE);
- errorResponse.entity("Message: " + e.getMessage());
- return errorResponse.build();
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/JacocoRuntimeController.java b/agent/src/main/java/com/teamscale/jacoco/agent/JacocoRuntimeController.java
deleted file mode 100644
index 1ad9b2146..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/JacocoRuntimeController.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/*-------------------------------------------------------------------------+
-| |
-| Copyright (c) 2009-2018 CQSE GmbH |
-| |
-+-------------------------------------------------------------------------*/
-package com.teamscale.jacoco.agent;
-
-import com.teamscale.report.jacoco.dump.Dump;
-import org.jacoco.agent.rt.IAgent;
-import org.jacoco.agent.rt.RT;
-import org.jacoco.core.data.ExecutionDataReader;
-import org.jacoco.core.data.ExecutionDataStore;
-import org.jacoco.core.data.ISessionInfoVisitor;
-import org.jacoco.core.data.SessionInfo;
-
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-
-/**
- * Wrapper around JaCoCo's {@link RT} runtime interface.
- *
- * Can be used if the calling code is run in the same JVM as the agent is attached to.
- */
-public class JacocoRuntimeController {
-
- /** Indicates a failed dump. */
- public static class DumpException extends Exception {
-
- /** Serialization ID. */
- private static final long serialVersionUID = 1L;
-
- /** Constructor. */
- public DumpException(String message, Throwable cause) {
- super(message, cause);
- }
-
- }
-
- /** JaCoCo's {@link RT} agent instance */
- private final IAgent agent;
-
- /** Constructor. */
- public JacocoRuntimeController(IAgent agent) {
- this.agent = agent;
- }
-
- /**
- * Dumps execution data and resets it.
- *
- * @throws DumpException if dumping fails. This should never happen in real life. Dumping should simply be retried
- * later if this ever happens.
- */
- public Dump dumpAndReset() throws DumpException {
- byte[] binaryData = agent.getExecutionData(true);
-
- try (ByteArrayInputStream inputStream = new ByteArrayInputStream(binaryData)) {
- ExecutionDataReader reader = new ExecutionDataReader(inputStream);
-
- ExecutionDataStore store = new ExecutionDataStore();
- reader.setExecutionDataVisitor(store::put);
-
- SessionInfoVisitor sessionInfoVisitor = new SessionInfoVisitor();
- reader.setSessionInfoVisitor(sessionInfoVisitor);
-
- reader.read();
- return new Dump(sessionInfoVisitor.sessionInfo, store);
- } catch (IOException e) {
- throw new DumpException("should never happen for the ByteArrayInputStream", e);
- }
- }
-
- /**
- * Dumps execution data to the given file and resets it afterwards.
- */
- public void dumpToFileAndReset(File file) throws IOException {
- byte[] binaryData = agent.getExecutionData(true);
-
- try (FileOutputStream outputStream = new FileOutputStream(file, true)) {
- outputStream.write(binaryData);
- }
- }
-
-
- /**
- * Dumps execution data to a file and resets it.
- *
- * @throws DumpException if dumping fails. This should never happen in real life. Dumping should simply be retried
- * later if this ever happens.
- */
- public void dump() throws DumpException {
- try {
- agent.dump(true);
- } catch (IOException e) {
- throw new DumpException(e.getMessage(), e);
- }
- }
-
- /** Resets already collected coverage. */
- public void reset() {
- agent.reset();
- }
-
- /** Returns the current sessionId. */
- public String getSessionId() {
- return agent.getSessionId();
- }
-
- /**
- * Sets the current sessionId of the agent that can be used to identify which coverage is recorded from now on.
- */
- public void setSessionId(String sessionId) {
- agent.setSessionId(sessionId);
- }
-
- /** Unsets the session ID so that coverage collected from now on is not attributed to the previous test. */
- public void resetSessionId() {
- agent.setSessionId("");
- }
-
- /**
- * Receives and stores a {@link SessionInfo}. Has a fallback dummy session in case nothing is received.
- */
- private static class SessionInfoVisitor implements ISessionInfoVisitor {
-
- /** The received session info or a dummy. */
- public SessionInfo sessionInfo = new SessionInfo("dummysession", System.currentTimeMillis(),
- System.currentTimeMillis());
-
- /** {@inheritDoc} */
- @Override
- public void visitSessionInfo(SessionInfo info) {
- this.sessionInfo = info;
- }
-
- }
-
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/LenientCoverageTransformer.java b/agent/src/main/java/com/teamscale/jacoco/agent/LenientCoverageTransformer.java
deleted file mode 100644
index 51c65737b..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/LenientCoverageTransformer.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package com.teamscale.jacoco.agent;
-
-import org.jacoco.agent.rt.internal_29a6edd.CoverageTransformer;
-import org.jacoco.agent.rt.internal_29a6edd.core.runtime.AgentOptions;
-import org.jacoco.agent.rt.internal_29a6edd.core.runtime.IRuntime;
-import org.slf4j.Logger;
-
-import java.lang.instrument.IllegalClassFormatException;
-import java.security.ProtectionDomain;
-
-/**
- * A class file transformer which delegates to the JaCoCo {@link CoverageTransformer} to do the actual instrumentation,
- * but treats instrumentation errors e.g. due to unsupported class file versions more lenient by only logging them, but
- * not bailing out completely. Those unsupported classes will not be instrumented and will therefore not be contained in
- * the collected coverage report.
- */
-public class LenientCoverageTransformer extends CoverageTransformer {
-
- private final Logger logger;
-
- public LenientCoverageTransformer(IRuntime runtime, AgentOptions options, Logger logger) {
- // The coverage transformer only uses the logger to print an error when the instrumentation fails.
- // We want to show our more specific error message instead, so we only log this for debugging at trace.
- super(runtime, options, e -> logger.trace(e.getMessage(), e));
- this.logger = logger;
- }
-
- @Override
- public byte[] transform(ClassLoader loader, String classname, Class> classBeingRedefined,
- ProtectionDomain protectionDomain,
- byte[] classfileBuffer) {
- try {
- return super.transform(loader, classname, classBeingRedefined, protectionDomain, classfileBuffer);
- } catch (IllegalClassFormatException e) {
- logger.error(
- "Failed to instrument " + classname + ". File will be skipped from instrumentation. " +
- "No coverage will be collected for it. Exclude the file from the instrumentation or try " +
- "updating the Teamscale Java Profiler if the file should actually be instrumented. (Cause: {})",
- getRootCauseMessage(e));
- return null;
- }
- }
-
- private static String getRootCauseMessage(Throwable e) {
- if (e.getCause() != null) {
- return getRootCauseMessage(e.getCause());
- }
- return e.getMessage();
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/Main.java b/agent/src/main/java/com/teamscale/jacoco/agent/Main.java
deleted file mode 100644
index 20c92dae8..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/Main.java
+++ /dev/null
@@ -1,84 +0,0 @@
-package com.teamscale.jacoco.agent;
-
-import com.beust.jcommander.JCommander;
-import com.beust.jcommander.JCommander.Builder;
-import com.beust.jcommander.Parameter;
-import com.beust.jcommander.ParameterException;
-import com.teamscale.client.StringUtils;
-import com.teamscale.jacoco.agent.commandline.Validator;
-import com.teamscale.jacoco.agent.convert.ConvertCommand;
-import com.teamscale.jacoco.agent.logging.LoggingUtils;
-import com.teamscale.jacoco.agent.util.AgentUtils;
-import org.jacoco.core.JaCoCo;
-import org.slf4j.Logger;
-
-/** Provides a command line interface for interacting with JaCoCo. */
-public class Main {
-
- /** The logger. */
- private final Logger logger = LoggingUtils.getLogger(this);
-
- /** The default arguments that will always be parsed. */
- private final DefaultArguments defaultArguments = new DefaultArguments();
-
- /** The arguments for the one-time conversion process. */
- private final ConvertCommand command = new ConvertCommand();
-
- /** Entry point. */
- public static void main(String[] args) throws Exception {
- new Main().parseCommandLineAndRun(args);
- }
-
- /**
- * Parses the given command line arguments. Exits the program or throws an exception if the arguments are not valid.
- * Then runs the specified command.
- */
- private void parseCommandLineAndRun(String[] args) throws Exception {
- Builder builder = createJCommanderBuilder();
- JCommander jCommander = builder.build();
-
- try {
- jCommander.parse(args);
- } catch (ParameterException e) {
- handleInvalidCommandLine(jCommander, e.getMessage());
- }
-
- if (defaultArguments.help) {
- System.out.println(
- "Teamscale Java Profiler " + AgentUtils.VERSION + " compiled against JaCoCo " + JaCoCo.VERSION);
- jCommander.usage();
- return;
- }
-
- Validator validator = command.validate();
- if (!validator.isValid()) {
- handleInvalidCommandLine(jCommander, StringUtils.LINE_FEED + validator.getErrorMessage());
- }
-
- logger.info(
- "Starting Teamscale Java Profiler " + AgentUtils.VERSION + " compiled against JaCoCo " + JaCoCo.VERSION);
- command.run();
- }
-
- /** Creates a builder for a {@link JCommander} object. */
- private Builder createJCommanderBuilder() {
- return JCommander.newBuilder().programName(Main.class.getName()).addObject(defaultArguments).addObject(command);
- }
-
- /** Shows an informative error and help message. Then exits the program. */
- private static void handleInvalidCommandLine(JCommander jCommander, String message) {
- System.err.println("Invalid command line: " + message + StringUtils.LINE_FEED);
- jCommander.usage();
- System.exit(1);
- }
-
- /** Default arguments that may always be provided. */
- private static class DefaultArguments {
-
- /** Shows the help message. */
- @Parameter(names = "--help", help = true, description = "Shows all available command line arguments.")
- private boolean help;
-
- }
-
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java b/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java
deleted file mode 100644
index 921a115b8..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java
+++ /dev/null
@@ -1,305 +0,0 @@
-package com.teamscale.jacoco.agent;
-
-import com.teamscale.client.FileSystemUtils;
-import com.teamscale.client.HttpUtils;
-import com.teamscale.client.StringUtils;
-import com.teamscale.jacoco.agent.configuration.AgentOptionReceiveException;
-import com.teamscale.jacoco.agent.logging.DebugLogDirectoryPropertyDefiner;
-import com.teamscale.jacoco.agent.logging.LogDirectoryPropertyDefiner;
-import com.teamscale.jacoco.agent.logging.LogToTeamscaleAppender;
-import com.teamscale.jacoco.agent.logging.LoggingUtils;
-import com.teamscale.jacoco.agent.options.AgentOptionParseException;
-import com.teamscale.jacoco.agent.options.AgentOptions;
-import com.teamscale.jacoco.agent.options.AgentOptionsParser;
-import com.teamscale.jacoco.agent.options.FilePatternResolver;
-import com.teamscale.jacoco.agent.options.JacocoAgentOptionsBuilder;
-import com.teamscale.jacoco.agent.options.TeamscaleCredentials;
-import com.teamscale.jacoco.agent.options.TeamscalePropertiesUtils;
-import com.teamscale.jacoco.agent.testimpact.TestwiseCoverageAgent;
-import com.teamscale.jacoco.agent.upload.UploaderException;
-import com.teamscale.jacoco.agent.util.AgentUtils;
-import com.teamscale.report.util.ILogger;
-import kotlin.Pair;
-import org.slf4j.Logger;
-
-import java.io.File;
-import java.io.IOException;
-import java.lang.instrument.Instrumentation;
-import java.lang.management.ManagementFactory;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.List;
-import java.util.Optional;
-import java.util.stream.Collectors;
-
-import static com.teamscale.jacoco.agent.logging.LoggingUtils.getLoggerContext;
-
-/** Container class for the premain entry point for the agent. */
-public class PreMain {
-
- private static LoggingUtils.LoggingResources loggingResources = null;
-
- /**
- * System property that we use to prevent this agent from being attached to the same VM twice. This can happen if
- * the agent is registered via multiple JVM environment variables and/or the command line at the same time.
- */
- private static final String LOCKING_SYSTEM_PROPERTY = "TEAMSCALE_JAVA_PROFILER_ATTACHED";
-
- /**
- * Environment variable from which to read the config ID to use. This is an ID for a profiler configuration that is
- * stored in Teamscale.
- */
- private static final String CONFIG_ID_ENVIRONMENT_VARIABLE = "TEAMSCALE_JAVA_PROFILER_CONFIG_ID";
-
- /** Environment variable from which to read the config file to use. */
- private static final String CONFIG_FILE_ENVIRONMENT_VARIABLE = "TEAMSCALE_JAVA_PROFILER_CONFIG_FILE";
-
- /** Environment variable from which to read the Teamscale access token. */
- private static final String ACCESS_TOKEN_ENVIRONMENT_VARIABLE = "TEAMSCALE_ACCESS_TOKEN";
-
- /**
- * Entry point for the agent, called by the JVM.
- */
- public static void premain(String options, Instrumentation instrumentation) throws Exception {
- if (System.getProperty(LOCKING_SYSTEM_PROPERTY) != null) {
- return;
- }
- System.setProperty(LOCKING_SYSTEM_PROPERTY, "true");
-
- String environmentConfigId = System.getenv(CONFIG_ID_ENVIRONMENT_VARIABLE);
- String environmentConfigFile = System.getenv(CONFIG_FILE_ENVIRONMENT_VARIABLE);
- if (StringUtils.isEmpty(options) && environmentConfigId == null && environmentConfigFile == null) {
- // profiler was registered globally and no config was set explicitly by the user, thus ignore this process
- // and don't profile anything
- return;
- }
-
- AgentOptions agentOptions = null;
- try {
- Pair> parseResult = getAndApplyAgentOptions(options, environmentConfigId,
- environmentConfigFile);
- agentOptions = parseResult.getFirst();
-
- // After parsing everything and configuring logging, we now
- // can throw the caught exceptions.
- for (Exception exception : parseResult.getSecond()) {
- throw exception;
- }
- } catch (AgentOptionParseException e) {
- getLoggerContext().getLogger(PreMain.class).error(e.getMessage(), e);
-
- // Flush logs to Teamscale, if configured.
- closeLoggingResources();
-
- // Unregister the profiler from Teamscale.
- if (agentOptions != null && agentOptions.configurationViaTeamscale != null) {
- agentOptions.configurationViaTeamscale.unregisterProfiler();
- }
-
- throw e;
- } catch (AgentOptionReceiveException e) {
- // When Teamscale is not available, we don't want to fail hard to still allow for testing even if no
- // coverage is collected (see TS-33237)
- return;
- }
-
- Logger logger = LoggingUtils.getLogger(Agent.class);
-
- logger.info("Teamscale Java profiler version " + AgentUtils.VERSION);
- logger.info("Starting JaCoCo's agent");
- JacocoAgentOptionsBuilder agentBuilder = new JacocoAgentOptionsBuilder(agentOptions);
- JaCoCoPreMain.premain(agentBuilder.createJacocoAgentOptions(), instrumentation, logger);
-
- if (agentOptions.configurationViaTeamscale != null) {
- agentOptions.configurationViaTeamscale.startHeartbeatThreadAndRegisterShutdownHook();
- }
- AgentBase agent = createAgent(agentOptions, instrumentation);
- agent.registerShutdownHook();
- }
-
- private static Pair> getAndApplyAgentOptions(String options,
- String environmentConfigId,
- String environmentConfigFile) throws AgentOptionParseException, IOException, AgentOptionReceiveException {
-
- DelayedLogger delayedLogger = new DelayedLogger();
- List javaAgents = ManagementFactory.getRuntimeMXBean().getInputArguments().stream().filter(
- s -> s.contains("-javaagent")).collect(Collectors.toList());
- // We allow multiple instances of the teamscale-jacoco-agent as we ensure with the #LOCKING_SYSTEM_PROPERTY to only use it once
- List differentAgents = javaAgents.stream()
- .filter(javaAgent -> !javaAgent.contains("teamscale-jacoco-agent.jar")).collect(
- Collectors.toList());
-
- if (!differentAgents.isEmpty()) {
- delayedLogger.warn(
- "Using multiple java agents could interfere with coverage recording: " +
- String.join(", ", differentAgents));
- }
- if (!javaAgents.get(0).contains("teamscale-jacoco-agent.jar")) {
- delayedLogger.warn("For best results consider registering the Teamscale Java Profiler first.");
- }
-
- TeamscaleCredentials credentials = TeamscalePropertiesUtils.parseCredentials();
- if (credentials == null) {
- // As many users still don't use the installer based setup, this log message will be shown in almost every log.
- // We use a debug log, as this message can be confusing for customers that think a teamscale.properties file is synonymous with a config file.
- delayedLogger.debug(
- "No explicit teamscale.properties file given. Looking for Teamscale credentials in a config file or via a command line argument. This is expected unless the installer based setup was used.");
- }
-
- String environmentAccessToken = System.getenv(ACCESS_TOKEN_ENVIRONMENT_VARIABLE);
-
- Pair> parseResult;
- AgentOptions agentOptions;
- try {
- parseResult = AgentOptionsParser.parse(
- options, environmentConfigId, environmentConfigFile, credentials, environmentAccessToken,
- delayedLogger);
- agentOptions = parseResult.getFirst();
- } catch (AgentOptionParseException e) {
- try (LoggingUtils.LoggingResources ignored = initializeFallbackLogging(options, delayedLogger)) {
- delayedLogger.errorAndStdErr("Failed to parse agent options: " + e.getMessage(), e);
- attemptLogAndThrow(delayedLogger);
- throw e;
- }
- } catch (AgentOptionReceiveException e) {
- try (LoggingUtils.LoggingResources ignored = initializeFallbackLogging(options, delayedLogger)) {
- delayedLogger.errorAndStdErr(
- e.getMessage() + " The application should start up normally, but NO coverage will be collected! Check the log file for details.",
- e);
- attemptLogAndThrow(delayedLogger);
- throw e;
- }
- }
-
- initializeLogging(agentOptions, delayedLogger);
- Logger logger = LoggingUtils.getLogger(Agent.class);
- delayedLogger.logTo(logger);
- HttpUtils.setShouldValidateSsl(agentOptions.shouldValidateSsl());
-
- return parseResult;
- }
-
- private static void attemptLogAndThrow(DelayedLogger delayedLogger) {
- // We perform actual logging output after writing to console to
- // ensure the console is reached even in case of logging issues
- // (see TS-23151). We use the Agent class here (same as below)
- Logger logger = LoggingUtils.getLogger(Agent.class);
- delayedLogger.logTo(logger);
- }
-
- /** Initializes logging during {@link #premain(String, Instrumentation)} and also logs the log directory. */
- private static void initializeLogging(AgentOptions agentOptions, DelayedLogger logger) throws IOException {
- if (agentOptions.isDebugLogging()) {
- initializeDebugLogging(agentOptions, logger);
- } else {
- loggingResources = LoggingUtils.initializeLogging(agentOptions.getLoggingConfig());
- logger.info("Logging to " + new LogDirectoryPropertyDefiner().getPropertyValue());
- }
-
- if (agentOptions.getTeamscaleServerOptions().isConfiguredForServerConnection()) {
- if (LogToTeamscaleAppender.addTeamscaleAppenderTo(getLoggerContext(), agentOptions)) {
- logger.info("Logs are being forwarded to Teamscale at " + agentOptions.getTeamscaleServerOptions().url);
- }
- }
- }
-
- /** Closes the opened logging contexts. */
- static void closeLoggingResources() {
- loggingResources.close();
- }
-
- /**
- * Returns in instance of the agent that was configured. Either an agent with interval based line-coverage dump or
- * the HTTP server is used.
- */
- private static AgentBase createAgent(AgentOptions agentOptions,
- Instrumentation instrumentation) throws UploaderException, IOException {
- if (agentOptions.useTestwiseCoverageMode()) {
- return TestwiseCoverageAgent.create(agentOptions);
- } else {
- return new Agent(agentOptions, instrumentation);
- }
- }
-
- /**
- * Initializes debug logging during {@link #premain(String, Instrumentation)} and also logs the log directory if
- * given.
- */
- private static void initializeDebugLogging(AgentOptions agentOptions, DelayedLogger logger) {
- loggingResources = LoggingUtils.initializeDebugLogging(agentOptions.getDebugLogDirectory());
- Path logDirectory = Paths.get(new DebugLogDirectoryPropertyDefiner().getPropertyValue());
- if (FileSystemUtils.isValidPath(logDirectory.toString()) && Files.isWritable(logDirectory)) {
- logger.info("Logging to " + logDirectory);
- } else {
- logger.warn("Could not create " + logDirectory + ". Logging to console only.");
- }
- }
-
- /**
- * Initializes fallback logging in case of an error during the parsing of the options to
- * {@link #premain(String, Instrumentation)} (see TS-23151). This tries to extract the logging configuration and use
- * this and falls back to the default logger.
- */
- private static LoggingUtils.LoggingResources initializeFallbackLogging(String premainOptions,
- DelayedLogger delayedLogger) {
- if (premainOptions == null) {
- return LoggingUtils.initializeDefaultLogging();
- }
- for (String optionPart : premainOptions.split(",")) {
- if (optionPart.startsWith(AgentOptionsParser.DEBUG + "=")) {
- String value = optionPart.split("=", 2)[1];
- boolean debugDisabled = value.equalsIgnoreCase("false");
- boolean debugEnabled = value.equalsIgnoreCase("true");
- if (debugDisabled) {
- continue;
- }
- Path debugLogDirectory = null;
- if (!value.isEmpty() && !debugEnabled) {
- debugLogDirectory = Paths.get(value);
- }
- return LoggingUtils.initializeDebugLogging(debugLogDirectory);
- }
- if (optionPart.startsWith(AgentOptionsParser.LOGGING_CONFIG_OPTION + "=")) {
- return createFallbackLoggerFromConfig(optionPart.split("=", 2)[1], delayedLogger);
- }
-
- if (optionPart.startsWith(AgentOptionsParser.CONFIG_FILE_OPTION + "=")) {
- String configFileValue = optionPart.split("=", 2)[1];
- Optional loggingConfigLine = Optional.empty();
- try {
- File configFile = new FilePatternResolver(delayedLogger).parsePath(
- AgentOptionsParser.CONFIG_FILE_OPTION, configFileValue).toFile();
- loggingConfigLine = FileSystemUtils.readLinesUTF8(configFile).stream()
- .filter(line -> line.startsWith(AgentOptionsParser.LOGGING_CONFIG_OPTION + "="))
- .findFirst();
- } catch (IOException e) {
- delayedLogger.error("Failed to load configuration from " + configFileValue + ": " + e.getMessage(),
- e);
- }
- if (loggingConfigLine.isPresent()) {
- return createFallbackLoggerFromConfig(loggingConfigLine.get().split("=", 2)[1], delayedLogger);
- }
- }
- }
-
- return LoggingUtils.initializeDefaultLogging();
- }
-
- /** Creates a fallback logger using the given config file. */
- private static LoggingUtils.LoggingResources createFallbackLoggerFromConfig(String configLocation,
- ILogger delayedLogger) {
- try {
- return LoggingUtils.initializeLogging(
- new FilePatternResolver(delayedLogger).parsePath(AgentOptionsParser.LOGGING_CONFIG_OPTION,
- configLocation));
- } catch (IOException e) {
- String message = "Failed to load log configuration from location " + configLocation + ": " + e.getMessage();
- delayedLogger.error(message, e);
- // output the message to console as well, as this might
- // otherwise not make it to the user
- System.err.println(message);
- return LoggingUtils.initializeDefaultLogging();
- }
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/ResourceBase.java b/agent/src/main/java/com/teamscale/jacoco/agent/ResourceBase.java
deleted file mode 100644
index fb539824e..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/ResourceBase.java
+++ /dev/null
@@ -1,145 +0,0 @@
-package com.teamscale.jacoco.agent;
-
-import com.teamscale.client.CommitDescriptor;
-import com.teamscale.client.StringUtils;
-import com.teamscale.client.TeamscaleServer;
-import com.teamscale.jacoco.agent.logging.LoggingUtils;
-import com.teamscale.jacoco.agent.testimpact.TestwiseCoverageAgent;
-import com.teamscale.report.testwise.model.RevisionInfo;
-import org.jetbrains.annotations.Contract;
-import org.slf4j.Logger;
-
-import javax.ws.rs.BadRequestException;
-import javax.ws.rs.GET;
-import javax.ws.rs.PUT;
-import javax.ws.rs.Path;
-import javax.ws.rs.Produces;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.Response;
-import java.util.Optional;
-
-
-/**
- * The resource of the Jersey + Jetty http server holding all the endpoints specific for the {@link AgentBase}.
- */
-public abstract class ResourceBase {
-
- /** The logger. */
- protected final Logger logger = LoggingUtils.getLogger(this);
-
- /**
- * The agentBase inject via {@link AgentResource#setAgent(Agent)} or
- * {@link com.teamscale.jacoco.agent.testimpact.TestwiseCoverageResource#setAgent(TestwiseCoverageAgent)}.
- */
- protected static AgentBase agentBase;
-
- /** Returns the partition for the Teamscale upload. */
- @GET
- @Path("/partition")
- public String getPartition() {
- return Optional.ofNullable(agentBase.options.getTeamscaleServerOptions().partition).orElse("");
- }
-
- /** Returns the upload message for the Teamscale upload. */
- @GET
- @Path("/message")
- public String getMessage() {
- return Optional.ofNullable(agentBase.options.getTeamscaleServerOptions().getMessage())
- .orElse("");
- }
-
- /** Returns revision information for the Teamscale upload. */
- @GET
- @Path("/revision")
- @Produces(MediaType.APPLICATION_JSON)
- public RevisionInfo getRevision() {
- return this.getRevisionInfo();
- }
-
- /** Returns revision information for the Teamscale upload. */
- @GET
- @Path("/commit")
- @Produces(MediaType.APPLICATION_JSON)
- public RevisionInfo getCommit() {
- return this.getRevisionInfo();
- }
-
- /** Handles setting the partition name. */
- @PUT
- @Path("/partition")
- public Response setPartition(String partitionString) {
- String partition = StringUtils.removeDoubleQuotes(partitionString);
- if (partition == null || partition.isEmpty()) {
- handleBadRequest("The new partition name is missing in the request body! Please add it as plain text.");
- }
-
- logger.debug("Changing partition name to " + partition);
- agentBase.dumpReport();
- agentBase.controller.setSessionId(partition);
- agentBase.options.getTeamscaleServerOptions().partition = partition;
- return Response.noContent().build();
- }
-
- /** Handles setting the upload message. */
- @PUT
- @Path("/message")
- public Response setMessage(String messageString) {
- String message = StringUtils.removeDoubleQuotes(messageString);
- if (message == null || message.isEmpty()) {
- handleBadRequest("The new message is missing in the request body! Please add it as plain text.");
- }
-
- agentBase.dumpReport();
- logger.debug("Changing message to " + message);
- agentBase.options.getTeamscaleServerOptions().setMessage(message);
-
- return Response.noContent().build();
- }
-
- /** Handles setting the revision. */
- @PUT
- @Path("/revision")
- public Response setRevision(String revisionString) {
- String revision = StringUtils.removeDoubleQuotes(revisionString);
- if (revision == null || revision.isEmpty()) {
- handleBadRequest("The new revision name is missing in the request body! Please add it as plain text.");
- }
-
- agentBase.dumpReport();
- logger.debug("Changing revision name to " + revision);
- agentBase.options.getTeamscaleServerOptions().revision = revision;
-
- return Response.noContent().build();
- }
-
- /** Handles setting the upload commit. */
- @PUT
- @Path("/commit")
- public Response setCommit(String commitString) {
- String commit = StringUtils.removeDoubleQuotes(commitString);
- if (commit == null || commit.isEmpty()) {
- handleBadRequest("The new upload commit is missing in the request body! Please add it as plain text.");
- }
-
- agentBase.dumpReport();
- agentBase.options.getTeamscaleServerOptions().commit = CommitDescriptor.parse(commit);
-
- return Response.noContent().build();
- }
-
- /** Returns revision information for the Teamscale upload. */
- private RevisionInfo getRevisionInfo() {
- TeamscaleServer server = agentBase.options.getTeamscaleServerOptions();
- return new RevisionInfo(server.commit, server.revision);
- }
-
- /**
- * Handles bad requests to the endpoints.
- */
- @Contract(value = "_ -> fail")
- protected void handleBadRequest(String message) throws BadRequestException {
- logger.error(message);
- throw new BadRequestException(message);
- }
-
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/commandline/Validator.java b/agent/src/main/java/com/teamscale/jacoco/agent/commandline/Validator.java
deleted file mode 100644
index 40b7ce388..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/commandline/Validator.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*-------------------------------------------------------------------------+
-| |
-| Copyright (c) 2009-2017 CQSE GmbH |
-| |
-+-------------------------------------------------------------------------*/
-package com.teamscale.jacoco.agent.commandline;
-
-import com.teamscale.client.StringUtils;
-import com.teamscale.jacoco.agent.util.Assertions;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Helper class to allow for multiple validations to occur.
- */
-public class Validator {
-
- /** The found validation problems in the form of error messages for the user. */
- private final List messages = new ArrayList<>();
-
- /** Runs the given validation routine. */
- public void ensure(ExceptionBasedValidation validation) {
- try {
- validation.validate();
- } catch (Exception | AssertionError e) {
- messages.add(e.getMessage());
- }
- }
-
- /**
- * Interface for a validation routine that throws an exception when it fails.
- */
- @FunctionalInterface
- public interface ExceptionBasedValidation {
-
- /**
- * Throws an {@link Exception} or {@link AssertionError} if the validation fails.
- */
- void validate() throws Exception, AssertionError;
-
- }
-
- /**
- * Checks that the given condition is true or adds the given error message.
- */
- public void isTrue(boolean condition, String message) {
- ensure(() -> Assertions.isTrue(condition, message));
- }
-
- /**
- * Checks that the given condition is false or adds the given error message.
- */
- public void isFalse(boolean condition, String message) {
- ensure(() -> Assertions.isFalse(condition, message));
- }
-
- /** Returns true if the validation succeeded. */
- public boolean isValid() {
- return messages.isEmpty();
- }
-
- /** Returns an error message with all validation problems that were found. */
- public String getErrorMessage() {
- return "- " + String.join(StringUtils.LINE_FEED + "- ", messages);
- }
-
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/CommitInfo.java b/agent/src/main/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/CommitInfo.java
deleted file mode 100644
index 82d96d480..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/CommitInfo.java
+++ /dev/null
@@ -1,57 +0,0 @@
-package com.teamscale.jacoco.agent.commit_resolution.git_properties;
-
-import com.teamscale.client.CommitDescriptor;
-import com.teamscale.client.StringUtils;
-
-import java.util.Objects;
-
-/** Hold information regarding a commit. */
-public class CommitInfo {
- /** The revision information (git hash). */
- public String revision;
-
- /** The commit descriptor. */
- public CommitDescriptor commit;
-
- /**
- * If the commit property is set via the teamscale.commit.branch and teamscale.commit.time
- * properties in a git.properties file, this should be preferred to the revision. For details see TS-38561.
- */
- public boolean preferCommitDescriptorOverRevision = false;
-
- /** Constructor. */
- public CommitInfo(String revision, CommitDescriptor commit) {
- this.revision = revision;
- this.commit = commit;
- }
-
- @Override
- public String toString() {
- return commit + "/" + revision;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
- CommitInfo that = (CommitInfo) o;
- return Objects.equals(revision, that.revision) && Objects.equals(commit, that.commit);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(revision, commit);
- }
-
- /**
- * Returns true if one of or both, revision and commit, are set
- */
- public boolean isEmpty() {
- return StringUtils.isEmpty(revision) && commit == null;
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitMultiProjectPropertiesLocator.java b/agent/src/main/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitMultiProjectPropertiesLocator.java
deleted file mode 100644
index cd41823d9..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitMultiProjectPropertiesLocator.java
+++ /dev/null
@@ -1,104 +0,0 @@
-package com.teamscale.jacoco.agent.commit_resolution.git_properties;
-
-import com.teamscale.jacoco.agent.logging.LoggingUtils;
-import com.teamscale.jacoco.agent.options.ProjectAndCommit;
-import com.teamscale.jacoco.agent.upload.teamscale.DelayedTeamscaleMultiProjectUploader;
-import com.teamscale.jacoco.agent.util.DaemonThreadFactory;
-import org.jetbrains.annotations.Nullable;
-import org.jetbrains.annotations.VisibleForTesting;
-import org.slf4j.Logger;
-
-import java.io.File;
-import java.io.IOException;
-import java.time.format.DateTimeFormatter;
-import java.util.List;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-
-/**
- * Searches a Jar/War/Ear/... file for a git.properties file in order to enable upload for the commit described therein,
- * e.g. to Teamscale, via a {@link DelayedTeamscaleMultiProjectUploader}. Specifically, this searches for the
- * 'teamscale.project' property specified in each of the discovered 'git.properties' files.
- */
-public class GitMultiProjectPropertiesLocator implements IGitPropertiesLocator {
-
- private final Logger logger = LoggingUtils.getLogger(this);
-
- private final Executor executor;
- private final DelayedTeamscaleMultiProjectUploader uploader;
-
- private final boolean recursiveSearch;
-
- private final @Nullable DateTimeFormatter gitPropertiesCommitTimeFormat;
-
- public GitMultiProjectPropertiesLocator(DelayedTeamscaleMultiProjectUploader uploader, boolean recursiveSearch, @Nullable DateTimeFormatter gitPropertiesCommitTimeFormat) {
- // using a single threaded executor allows this class to be lock-free
- this(uploader, Executors
- .newSingleThreadExecutor(
- new DaemonThreadFactory(GitMultiProjectPropertiesLocator.class,
- "git.properties Jar scanner thread")), recursiveSearch, gitPropertiesCommitTimeFormat);
- }
-
- public GitMultiProjectPropertiesLocator(DelayedTeamscaleMultiProjectUploader uploader, Executor executor,
- boolean recursiveSearch, @Nullable DateTimeFormatter gitPropertiesCommitTimeFormat) {
- this.uploader = uploader;
- this.executor = executor;
- this.recursiveSearch = recursiveSearch;
- this.gitPropertiesCommitTimeFormat = gitPropertiesCommitTimeFormat;
- }
-
- /**
- * Asynchronously searches the given jar file for git.properties files and adds a corresponding uploader to the
- * multi-project uploader.
- */
- @Override
- public void searchFileForGitPropertiesAsync(File file, boolean isJarFile) {
- executor.execute(() -> searchFile(file, isJarFile));
- }
-
- /**
- * Synchronously searches the given jar file for git.properties files and adds a corresponding uploader to the
- * multi-project uploader.
- */
- @VisibleForTesting
- void searchFile(File file, boolean isJarFile) {
- logger.debug("Searching file {} for multiple git.properties", file.toString());
- try {
- List projectAndCommits = GitPropertiesLocatorUtils.getProjectRevisionsFromGitProperties(
- file,
- isJarFile,
- recursiveSearch, gitPropertiesCommitTimeFormat);
- if (projectAndCommits.isEmpty()) {
- logger.debug("No git.properties file found in {}", file);
- return;
- }
-
- for (ProjectAndCommit projectAndCommit : projectAndCommits) {
- // this code only runs when 'teamscale-project' is not given via the agent properties,
- // i.e., a multi-project upload is being attempted.
- // Therefore, we expect to find both the project (teamscale.project) and the revision
- // (git.commit.id) in the git.properties file.
- if (projectAndCommit.getProject() == null || projectAndCommit.getCommitInfo() == null) {
- logger.debug(
- "Found inconsistent git.properties file: the git.properties file in {} either does not specify the" +
- " Teamscale project ({}) property, or does not specify the commit " +
- "({}, {} + {}, or {} + {})." +
- " Will skip this git.properties file and try to continue with the other ones that were found during discovery.",
- file, GitPropertiesLocatorUtils.GIT_PROPERTIES_TEAMSCALE_PROJECT,
- GitPropertiesLocatorUtils.GIT_PROPERTIES_GIT_COMMIT_ID,
- GitPropertiesLocatorUtils.GIT_PROPERTIES_GIT_BRANCH,
- GitPropertiesLocatorUtils.GIT_PROPERTIES_GIT_COMMIT_TIME,
- GitPropertiesLocatorUtils.GIT_PROPERTIES_TEAMSCALE_COMMIT_BRANCH,
- GitPropertiesLocatorUtils.GIT_PROPERTIES_TEAMSCALE_COMMIT_TIME);
- continue;
- }
- logger.debug("Found git.properties file in {} and found Teamscale project {} and revision {}", file,
- projectAndCommit.getProject(), projectAndCommit.getCommitInfo());
- uploader.addTeamscaleProjectAndCommit(file, projectAndCommit);
- }
- } catch (IOException | InvalidGitPropertiesException e) {
- logger.error("Error during asynchronous search for git.properties in {}", file, e);
- }
- }
-
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitPropertiesLocatingTransformer.java b/agent/src/main/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitPropertiesLocatingTransformer.java
deleted file mode 100644
index 2b430047e..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitPropertiesLocatingTransformer.java
+++ /dev/null
@@ -1,87 +0,0 @@
-package com.teamscale.jacoco.agent.commit_resolution.git_properties;
-
-import com.teamscale.client.StringUtils;
-import com.teamscale.jacoco.agent.logging.LoggingUtils;
-import com.teamscale.report.util.ClasspathWildcardIncludeFilter;
-import kotlin.Pair;
-import org.slf4j.Logger;
-
-import java.io.File;
-import java.lang.instrument.ClassFileTransformer;
-import java.net.URL;
-import java.security.CodeSource;
-import java.security.ProtectionDomain;
-import java.util.Set;
-import java.util.concurrent.ConcurrentSkipListSet;
-
-/**
- * {@link ClassFileTransformer} that doesn't change the loaded classes but searches their corresponding Jar/War/Ear/...
- * files for a git.properties file.
- */
-public class GitPropertiesLocatingTransformer implements ClassFileTransformer {
-
- private final Logger logger = LoggingUtils.getLogger(this);
- private final Set seenJars = new ConcurrentSkipListSet<>();
- private final IGitPropertiesLocator locator;
- private final ClasspathWildcardIncludeFilter locationIncludeFilter;
-
- public GitPropertiesLocatingTransformer(IGitPropertiesLocator locator,
- ClasspathWildcardIncludeFilter locationIncludeFilter) {
- this.locator = locator;
- this.locationIncludeFilter = locationIncludeFilter;
- }
-
- @Override
- public byte[] transform(ClassLoader classLoader, String className, Class> aClass,
- ProtectionDomain protectionDomain, byte[] classFileContent) {
- if (protectionDomain == null) {
- // happens for e.g. java.lang. We can ignore these classes
- return null;
- }
-
- if (StringUtils.isEmpty(className) || !locationIncludeFilter.isIncluded(className)) {
- // only search in jar files of included classes
- return null;
- }
-
- try {
- CodeSource codeSource = protectionDomain.getCodeSource();
- if (codeSource == null || codeSource.getLocation() == null) {
- // unknown when this can happen, we suspect when code is generated at runtime
- // but there's nothing else we can do here in either case.
- // codeSource.getLocation() is null e.g. when executing Pixelitor with Java14 for class sun/reflect/misc/Trampoline
- logger.debug("Could not locate code source for class {}. Skipping git.properties search for this class",
- className);
- return null;
- }
-
- URL jarOrClassFolderUrl = codeSource.getLocation();
- Pair searchRoot = GitPropertiesLocatorUtils.extractGitPropertiesSearchRoot(
- jarOrClassFolderUrl);
- if (searchRoot == null || searchRoot.getFirst() == null) {
- logger.warn("Not searching location for git.properties with unknown protocol or extension {}." +
- " If this location contains your git.properties, please report this warning as a" +
- " bug to CQSE. In that case, auto-discovery of git.properties will not work.",
- jarOrClassFolderUrl);
- return null;
- }
-
- if (hasLocationAlreadyBeenSearched(searchRoot.getFirst())) {
- return null;
- }
-
- logger.debug("Scheduling asynchronous search for git.properties in {}", searchRoot);
- locator.searchFileForGitPropertiesAsync(searchRoot.getFirst(), searchRoot.getSecond());
- } catch (Throwable e) {
- // 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);
- }
- return null;
- }
-
- private boolean hasLocationAlreadyBeenSearched(File location) {
- return !seenJars.add(location.toString());
- }
-
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitPropertiesLocatorUtils.java b/agent/src/main/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitPropertiesLocatorUtils.java
deleted file mode 100644
index 0d4080b06..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitPropertiesLocatorUtils.java
+++ /dev/null
@@ -1,462 +0,0 @@
-package com.teamscale.jacoco.agent.commit_resolution.git_properties;
-
-import com.teamscale.client.CommitDescriptor;
-import com.teamscale.client.FileSystemUtils;
-import com.teamscale.client.StringUtils;
-import com.teamscale.jacoco.agent.options.ProjectAndCommit;
-import com.teamscale.report.util.BashFileSkippingInputStream;
-import kotlin.Pair;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.time.ZonedDateTime;
-import java.time.format.DateTimeFormatter;
-import java.time.format.DateTimeFormatterBuilder;
-import java.time.format.DateTimeParseException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Properties;
-import java.util.jar.JarEntry;
-import java.util.jar.JarInputStream;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/** Utility methods to extract certain properties from git.properties files in archives and folders. */
-public class GitPropertiesLocatorUtils {
-
- /** Name of the git.properties file. */
- public static final String GIT_PROPERTIES_FILE_NAME = "git.properties";
-
- /** The git.properties key that holds the commit time. */
- public static final String GIT_PROPERTIES_GIT_COMMIT_TIME = "git.commit.time";
-
- /** The git.properties key that holds the commit branch. */
- public static final String GIT_PROPERTIES_GIT_BRANCH = "git.branch";
-
- /** The git.properties key that holds the commit hash. */
- public static final String GIT_PROPERTIES_GIT_COMMIT_ID = "git.commit.id";
-
- /**
- * Alternative git.properties key that might also hold the commit hash, depending on the Maven git-commit-id plugin
- * configuration.
- */
- public static final String GIT_PROPERTIES_GIT_COMMIT_ID_FULL = "git.commit.id.full";
-
- /**
- * You can provide a teamscale timestamp in git.properties files to overwrite the revision. See TS-38561.
- */
- public static final String GIT_PROPERTIES_TEAMSCALE_COMMIT_BRANCH = "teamscale.commit.branch";
-
- /**
- * You can provide a teamscale timestamp in git.properties files to overwrite the revision. See TS-38561.
- */
- public static final String GIT_PROPERTIES_TEAMSCALE_COMMIT_TIME = "teamscale.commit.time";
-
- /** The git.properties key that holds the Teamscale project name. */
- public static final String GIT_PROPERTIES_TEAMSCALE_PROJECT = "teamscale.project";
-
- /** Matches the path to the jar file in a jar:file: URL in regex group 1. */
- private static final Pattern JAR_URL_REGEX = Pattern.compile("jar:(?:file|nested):(.*?)!.*",
- Pattern.CASE_INSENSITIVE);
-
- private static final Pattern NESTED_JAR_REGEX = Pattern.compile("[jwea]ar:file:(.*?)\\*(.*)",
- Pattern.CASE_INSENSITIVE);
-
- /**
- * Defined in GitCommitIdMojo
- */
- private static final String GIT_PROPERTIES_DEFAULT_MAVEN_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssXXX";
-
- /**
- * Defined in GitPropertiesPlugin
- */
- private static final String GIT_PROPERTIES_DEFAULT_GRADLE_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ";
-
- /**
- * Reads the git SHA1 and branch and timestamp from the given jar file's git.properties and builds a commit
- * descriptor out of it. If no git.properties file can be found, returns null.
- *
- * @throws IOException If reading the jar file fails.
- * @throws InvalidGitPropertiesException If a git.properties file is found but it is malformed.
- */
- public static List getCommitInfoFromGitProperties(File file, boolean isJarFile,
- boolean recursiveSearch,
- @Nullable DateTimeFormatter gitPropertiesCommitTimeFormat)
- throws IOException, InvalidGitPropertiesException {
- List> entriesWithProperties = GitPropertiesLocatorUtils.findGitPropertiesInFile(file,
- isJarFile, recursiveSearch);
- List result = new ArrayList<>();
-
- for (Pair entryWithProperties : entriesWithProperties) {
- String entry = entryWithProperties.getFirst();
- Properties properties = entryWithProperties.getSecond();
-
- CommitInfo commitInfo = GitPropertiesLocatorUtils.getCommitInfoFromGitProperties(properties, entry, file,
- gitPropertiesCommitTimeFormat);
- result.add(commitInfo);
- }
-
- return result;
- }
-
- /**
- * Tries to extract a file system path to a search root for the git.properties search. A search root is either a
- * file system folder or a Jar file. If no such path can be extracted, returns null.
- *
- * @throws URISyntaxException under certain circumstances if parsing the URL fails. This should be treated the same
- * as a null search result but the exception is preserved so it can be logged.
- */
- public static Pair extractGitPropertiesSearchRoot(
- URL jarOrClassFolderUrl) throws URISyntaxException, IOException, NoSuchMethodException,
- IllegalAccessException, InvocationTargetException {
- String protocol = jarOrClassFolderUrl.getProtocol().toLowerCase();
- switch (protocol) {
- case "file":
- File jarOrClassFolderFile = new File(jarOrClassFolderUrl.toURI());
- if (jarOrClassFolderFile.isDirectory() || isJarLikeFile(jarOrClassFolderUrl.getPath())) {
- return new Pair<>(new File(jarOrClassFolderUrl.toURI()), !jarOrClassFolderFile.isDirectory());
- }
- break;
- case "jar":
- // Used e.g. by Spring Boot. Example: jar:file:/home/k/demo.jar!/BOOT-INF/classes!/
- Matcher jarMatcher = JAR_URL_REGEX.matcher(jarOrClassFolderUrl.toString());
- if (jarMatcher.matches()) {
- return new Pair<>(new File(jarMatcher.group(1)), true);
- }
- // Intentionally no break to handle ear and war files
- case "war":
- case "ear":
- // Used by some web applications and potentially fat jars.
- // Example: war:file:/Users/example/apache-tomcat/webapps/demo.war*/WEB-INF/lib/demoLib-1.0-SNAPSHOT.jar
- Matcher nestedMatcher = NESTED_JAR_REGEX.matcher(jarOrClassFolderUrl.toString());
- if (nestedMatcher.matches()) {
- return new Pair<>(new File(nestedMatcher.group(1)), true);
- }
- break;
- case "vfs":
- return getVfsContentFolder(jarOrClassFolderUrl);
- default:
- return null;
- }
- return null;
- }
-
- /**
- * VFS (Virtual File System) protocol is used by JBoss EAP and Wildfly. Example of an URL:
- * vfs:/content/helloworld.war/WEB-INF/classes
- */
- private static Pair getVfsContentFolder(
- URL jarOrClassFolderUrl) throws IOException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
- // we obtain the URL of a specific class file as input, e.g.,
- // vfs:/content/helloworld.war/WEB-INF/classes
- // Next, we try to extract the artefact URL from it, e.g., vfs:/content/helloworld.war
- String artefactUrl = extractArtefactUrl(jarOrClassFolderUrl);
-
- Object virtualFile = new URL(artefactUrl).openConnection().getContent();
- Class> virtualFileClass = virtualFile.getClass();
- // obtain the physical location of the class file. It is created on demand in /standalone/tmp/vfs
- Method getPhysicalFileMethod = virtualFileClass.getMethod("getPhysicalFile");
- File file = (File) getPhysicalFileMethod.invoke(virtualFile);
- return new Pair<>(file, !file.isDirectory());
- }
-
- /**
- * Extracts the artefact URL (e.g., vfs:/content/helloworld.war/) from the full URL of the class file (e.g.,
- * vfs:/content/helloworld.war/WEB-INF/classes).
- */
- private static String extractArtefactUrl(URL jarOrClassFolderUrl) {
- String url = jarOrClassFolderUrl.getPath().toLowerCase();
- String[] pathSegments = url.split("/");
- StringBuilder artefactUrlBuilder = new StringBuilder("vfs:");
- int segmentIdx = 0;
- while (segmentIdx < pathSegments.length) {
- String segment = pathSegments[segmentIdx];
- artefactUrlBuilder.append(segment);
- artefactUrlBuilder.append("/");
- if (isJarLikeFile(segment)) {
- break;
- }
- segmentIdx += 1;
- }
- if (segmentIdx == pathSegments.length) {
- return url;
- }
- return artefactUrlBuilder.toString();
- }
-
- private static boolean isJarLikeFile(String segment) {
- return StringUtils.endsWithOneOf(
- segment.toLowerCase(), ".jar", ".war", ".ear", ".aar");
- }
-
- /**
- * Reads the 'teamscale.project' property values and the git SHA1s or branch + timestamp from all git.properties
- * files contained in the provided folder or archive file.
- *
- * @throws IOException If reading the jar file fails.
- * @throws InvalidGitPropertiesException If a git.properties file is found but it is malformed.
- */
- public static List getProjectRevisionsFromGitProperties(
- File file, boolean isJarFile, boolean recursiveSearch,
- @Nullable DateTimeFormatter gitPropertiesCommitTimeFormat) throws IOException, InvalidGitPropertiesException {
- List> entriesWithProperties = findGitPropertiesInFile(file, isJarFile,
- recursiveSearch);
- List result = new ArrayList<>();
- for (Pair entryWithProperties : entriesWithProperties) {
- CommitInfo commitInfo = getCommitInfoFromGitProperties(entryWithProperties.getSecond(),
- entryWithProperties.getFirst(), file, gitPropertiesCommitTimeFormat);
- String project = entryWithProperties.getSecond().getProperty(GIT_PROPERTIES_TEAMSCALE_PROJECT);
- if (commitInfo.isEmpty() && StringUtils.isEmpty(project)) {
- throw new InvalidGitPropertiesException(
- "No entry or empty value for both '" + GIT_PROPERTIES_GIT_COMMIT_ID + "'/'" + GIT_PROPERTIES_GIT_COMMIT_ID_FULL +
- "' and '" + GIT_PROPERTIES_TEAMSCALE_PROJECT + "' in " + file + "." +
- "\nContents of " + GIT_PROPERTIES_FILE_NAME + ": " + entryWithProperties.getSecond()
- );
- }
- result.add(new ProjectAndCommit(project, commitInfo));
- }
- return result;
- }
-
- /**
- * Returns pairs of paths to git.properties files and their parsed properties found in the provided folder or
- * archive file. Nested jar files will also be searched recursively if specified.
- */
- public static List> findGitPropertiesInFile(
- File file, boolean isJarFile, boolean recursiveSearch) throws IOException {
- if (isJarFile) {
- return findGitPropertiesInArchiveFile(file, recursiveSearch);
- }
- return findGitPropertiesInDirectoryFile(file, recursiveSearch);
- }
-
- /**
- * Searches for git properties in jar/war/ear/aar files
- */
- private static List> findGitPropertiesInArchiveFile(File file,
- boolean recursiveSearch) throws IOException {
- try (JarInputStream jarStream = new JarInputStream(
- new BashFileSkippingInputStream(Files.newInputStream(file.toPath())))) {
- return findGitPropertiesInArchive(jarStream, file.getName(), recursiveSearch);
- } catch (IOException e) {
- throw new IOException("Reading jar " + file.getAbsolutePath() + " for obtaining commit " +
- "descriptor from git.properties failed", e);
- }
- }
-
- /**
- * Searches for git.properties file in the given folder
- *
- * @param recursiveSearch If enabled, git.properties files will also be searched in jar files
- */
- private static List> findGitPropertiesInDirectoryFile(
- File directoryFile, boolean recursiveSearch) throws IOException {
- List> result = new ArrayList<>(findGitPropertiesInFolder(directoryFile));
-
- if (recursiveSearch) {
- result.addAll(findGitPropertiesInNestedJarFiles(directoryFile));
- }
-
- return result;
- }
-
- /**
- * Finds all jar files in the given folder and searches them recursively for git.properties
- */
- private static List> findGitPropertiesInNestedJarFiles(
- File directoryFile) throws IOException {
- List> result = new ArrayList<>();
- List jarFiles = FileSystemUtils.listFilesRecursively(directoryFile,
- file -> isJarLikeFile(file.getName()));
- for (File jarFile : jarFiles) {
- JarInputStream is = new JarInputStream(Files.newInputStream(jarFile.toPath()));
- String relativeFilePath = directoryFile.getName() + File.separator + directoryFile.toPath()
- .relativize(jarFile.toPath());
- result.addAll(findGitPropertiesInArchive(is, relativeFilePath, true));
- }
- return result;
- }
-
- /**
- * Searches for git.properties files in the given folder
- */
- private static List> findGitPropertiesInFolder(File directoryFile) throws IOException {
- List> result = new ArrayList<>();
- List gitPropertiesFiles = FileSystemUtils.listFilesRecursively(directoryFile,
- file -> file.getName().equalsIgnoreCase(GIT_PROPERTIES_FILE_NAME));
- for (File gitPropertiesFile : gitPropertiesFiles) {
- try (InputStream is = Files.newInputStream(gitPropertiesFile.toPath())) {
- Properties gitProperties = new Properties();
- gitProperties.load(is);
- String relativeFilePath = directoryFile.getName() + File.separator + directoryFile.toPath()
- .relativize(gitPropertiesFile.toPath());
- result.add(new Pair<>(relativeFilePath, gitProperties));
- } catch (IOException e) {
- throw new IOException(
- "Reading directory " + gitPropertiesFile.getAbsolutePath() + " for obtaining commit " +
- "descriptor from git.properties failed", e);
- }
- }
- return result;
- }
-
- /**
- * Returns pairs of paths to git.properties files and their parsed properties found in the provided JarInputStream.
- * Nested jar files will also be searched recursively if specified.
- */
- static List> findGitPropertiesInArchive(
- JarInputStream in, String archiveName, boolean recursiveSearch) throws IOException {
- List> result = new ArrayList<>();
- JarEntry entry;
- boolean isEmpty = true;
-
- while ((entry = in.getNextJarEntry()) != null) {
- isEmpty = false;
- String fullEntryName = archiveName + File.separator + entry.getName();
- if (Paths.get(entry.getName()).getFileName().toString().equalsIgnoreCase(GIT_PROPERTIES_FILE_NAME)) {
- Properties gitProperties = new Properties();
- gitProperties.load(in);
- result.add(new Pair<>(fullEntryName, gitProperties));
- } else if (isJarLikeFile(entry.getName()) && recursiveSearch) {
- result.addAll(findGitPropertiesInArchive(new JarInputStream(in), fullEntryName, true));
- }
- }
- if (isEmpty) {
- throw new IOException(
- "No entries in Jar file " + archiveName + ". Is this a valid jar file?. If so, please report to CQSE.");
- }
- return result;
- }
-
- /**
- * Returns the CommitInfo (revision and branch + timestmap) from a git properties file. The revision can be either
- * in {@link #GIT_PROPERTIES_GIT_COMMIT_ID} or {@link #GIT_PROPERTIES_GIT_COMMIT_ID_FULL}. The branch and timestamp
- * in {@link #GIT_PROPERTIES_GIT_BRANCH} + {@link #GIT_PROPERTIES_GIT_COMMIT_TIME} or in
- * {@link #GIT_PROPERTIES_TEAMSCALE_COMMIT_BRANCH} + {@link #GIT_PROPERTIES_TEAMSCALE_COMMIT_TIME}. By default,
- * times will be parsed with {@link #GIT_PROPERTIES_DEFAULT_GRADLE_DATE_FORMAT} and
- * {@link #GIT_PROPERTIES_DEFAULT_MAVEN_DATE_FORMAT}. An additional format can be given with
- * {@code dateTimeFormatter}
- */
- public static CommitInfo getCommitInfoFromGitProperties(
- Properties gitProperties, String entryName, File jarFile,
- @Nullable DateTimeFormatter additionalDateTimeFormatter) throws InvalidGitPropertiesException {
-
- DateTimeFormatter dateTimeFormatter = createDateTimeFormatter(additionalDateTimeFormatter);
-
- // Get Revision
- String revision = getRevisionFromGitProperties(gitProperties);
-
- // Get branch and timestamp from git.commit.branch and git.commit.id
- CommitDescriptor commitDescriptor = getCommitDescriptorFromDefaultGitPropertyValues(gitProperties, entryName,
- jarFile, dateTimeFormatter);
- // When read from these properties, we should prefer to upload to the revision
- boolean preferCommitDescriptorOverRevision = false;
-
-
- // Get branch and timestamp from teamscale.commit.branch and teamscale.commit.time (TS-38561)
- CommitDescriptor teamscaleTimestampBasedCommitDescriptor = getCommitDescriptorFromTeamscaleTimestampProperty(
- gitProperties, entryName, jarFile, dateTimeFormatter);
- if (teamscaleTimestampBasedCommitDescriptor != null) {
- // In this case, as we specifically set this property, we should prefer branch and timestamp to the revision
- preferCommitDescriptorOverRevision = true;
- commitDescriptor = teamscaleTimestampBasedCommitDescriptor;
- }
-
- if (StringUtils.isEmpty(revision) && commitDescriptor == null) {
- throw new InvalidGitPropertiesException(
- "No entry or invalid value for '" + GIT_PROPERTIES_GIT_COMMIT_ID + "', '" + GIT_PROPERTIES_GIT_COMMIT_ID_FULL +
- "', '" + GIT_PROPERTIES_TEAMSCALE_COMMIT_BRANCH + "' and " + GIT_PROPERTIES_TEAMSCALE_COMMIT_TIME + "'\n" +
- "Location: Entry '" + entryName + "' in jar file '" + jarFile + "'." +
- "\nContents of " + GIT_PROPERTIES_FILE_NAME + ":\n" + gitProperties);
- }
-
- CommitInfo commitInfo = new CommitInfo(revision, commitDescriptor);
- commitInfo.preferCommitDescriptorOverRevision = preferCommitDescriptorOverRevision;
- return commitInfo;
- }
-
- private static @NotNull DateTimeFormatter createDateTimeFormatter(
- @org.jetbrains.annotations.Nullable DateTimeFormatter additionalDateTimeFormatter) {
- DateTimeFormatter defaultDateTimeFormatter = DateTimeFormatter.ofPattern(
- String.format("[%s][%s]", GIT_PROPERTIES_DEFAULT_MAVEN_DATE_FORMAT,
- GIT_PROPERTIES_DEFAULT_GRADLE_DATE_FORMAT));
- DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder().append(defaultDateTimeFormatter);
- if (additionalDateTimeFormatter != null) {
- builder.append(additionalDateTimeFormatter);
- }
- return builder.toFormatter();
- }
-
- private static String getRevisionFromGitProperties(Properties gitProperties) {
- String revision = gitProperties.getProperty(GIT_PROPERTIES_GIT_COMMIT_ID);
- if (StringUtils.isEmpty(revision)) {
- revision = gitProperties.getProperty(GIT_PROPERTIES_GIT_COMMIT_ID_FULL);
- }
- return revision;
- }
-
- private static CommitDescriptor getCommitDescriptorFromTeamscaleTimestampProperty(Properties gitProperties,
- String entryName, File jarFile,
- DateTimeFormatter dateTimeFormatter) throws InvalidGitPropertiesException {
- String teamscaleCommitBranch = gitProperties.getProperty(GIT_PROPERTIES_TEAMSCALE_COMMIT_BRANCH);
- String teamscaleCommitTime = gitProperties.getProperty(GIT_PROPERTIES_TEAMSCALE_COMMIT_TIME);
-
- if (StringUtils.isEmpty(teamscaleCommitBranch) || StringUtils.isEmpty(teamscaleCommitTime)) {
- return null;
- }
-
- String teamscaleTimestampRegex = "\\d*(?:p\\d*)?";
- Matcher teamscaleTimestampMatcher = Pattern.compile(teamscaleTimestampRegex).matcher(teamscaleCommitTime);
- if (teamscaleTimestampMatcher.matches()) {
- return new CommitDescriptor(teamscaleCommitBranch, teamscaleCommitTime);
- }
-
- long epochTimestamp;
- try {
- epochTimestamp = ZonedDateTime.parse(teamscaleCommitTime, dateTimeFormatter).toInstant().toEpochMilli();
- } catch (DateTimeParseException e) {
- throw new InvalidGitPropertiesException(
- "Cannot parse commit time '" + teamscaleCommitTime + "' in the '" + GIT_PROPERTIES_TEAMSCALE_COMMIT_TIME +
- "' property. It needs to be in the date formats '" + GIT_PROPERTIES_DEFAULT_MAVEN_DATE_FORMAT +
- "' or '" + GIT_PROPERTIES_DEFAULT_GRADLE_DATE_FORMAT + "' or match the Teamscale timestamp format '"
- + teamscaleTimestampRegex + "'." +
- "\nLocation: Entry '" + entryName + "' in jar file '" + jarFile + "'." +
- "\nContents of " + GIT_PROPERTIES_FILE_NAME + ":\n" + gitProperties, e);
- }
-
- return new CommitDescriptor(teamscaleCommitBranch, epochTimestamp);
- }
-
- private static CommitDescriptor getCommitDescriptorFromDefaultGitPropertyValues(Properties gitProperties,
- String entryName, File jarFile,
- DateTimeFormatter dateTimeFormatter) throws InvalidGitPropertiesException {
- String gitBranch = gitProperties.getProperty(GIT_PROPERTIES_GIT_BRANCH);
- String gitTime = gitProperties.getProperty(GIT_PROPERTIES_GIT_COMMIT_TIME);
- if (!StringUtils.isEmpty(gitBranch) && !StringUtils.isEmpty(gitTime)) {
- long gitTimestamp;
- try {
- gitTimestamp = ZonedDateTime.parse(gitTime, dateTimeFormatter).toInstant().toEpochMilli();
- } catch (DateTimeParseException e) {
- throw new InvalidGitPropertiesException(
- "Could not parse the timestamp in property '" + GIT_PROPERTIES_GIT_COMMIT_TIME + "'." +
- "\nLocation: Entry '" + entryName + "' in jar file '" + jarFile + "'." +
- "\nContents of " + GIT_PROPERTIES_FILE_NAME + ":\n" + gitProperties, e);
- }
- return new CommitDescriptor(gitBranch, gitTimestamp);
- }
- return null;
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitSingleProjectPropertiesLocator.java b/agent/src/main/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitSingleProjectPropertiesLocator.java
deleted file mode 100644
index e9b654670..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitSingleProjectPropertiesLocator.java
+++ /dev/null
@@ -1,115 +0,0 @@
-package com.teamscale.jacoco.agent.commit_resolution.git_properties;
-
-import com.teamscale.jacoco.agent.logging.LoggingUtils;
-import com.teamscale.jacoco.agent.upload.delay.DelayedUploader;
-import com.teamscale.jacoco.agent.util.DaemonThreadFactory;
-import org.jetbrains.annotations.Nullable;
-import org.slf4j.Logger;
-
-import java.io.File;
-import java.io.IOException;
-import java.time.format.DateTimeFormatter;
-import java.util.List;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-
-/**
- * Searches a Jar/War/Ear/... file for a git.properties file in order to enable upload for the commit described therein,
- * e.g. to Teamscale, via a {@link DelayedUploader}.
- */
-public class GitSingleProjectPropertiesLocator implements IGitPropertiesLocator {
-
- private final Logger logger = LoggingUtils.getLogger(this);
- private final Executor executor;
- private T foundData = null;
- private File jarFileWithGitProperties = null;
-
- private final DelayedUploader uploader;
- private final DataExtractor dataExtractor;
-
- private final boolean recursiveSearch;
- private final @Nullable DateTimeFormatter gitPropertiesCommitTimeFormat;
-
- public GitSingleProjectPropertiesLocator(DelayedUploader uploader, DataExtractor dataExtractor,
- boolean recursiveSearch,
- @Nullable DateTimeFormatter gitPropertiesCommitTimeFormat) {
- // using a single threaded executor allows this class to be lock-free
- this(uploader, dataExtractor, Executors
- .newSingleThreadExecutor(
- new DaemonThreadFactory(GitSingleProjectPropertiesLocator.class,
- "git.properties Jar scanner thread")),
- recursiveSearch, gitPropertiesCommitTimeFormat);
- }
-
- /**
- * Visible for testing. Allows tests to control the {@link Executor} in order to test the asynchronous functionality
- * of this class.
- */
- public GitSingleProjectPropertiesLocator(DelayedUploader uploader, DataExtractor dataExtractor,
- Executor executor,
- boolean recursiveSearch,
- @Nullable DateTimeFormatter gitPropertiesCommitTimeFormat) {
- this.uploader = uploader;
- this.dataExtractor = dataExtractor;
- this.executor = executor;
- this.recursiveSearch = recursiveSearch;
- this.gitPropertiesCommitTimeFormat = gitPropertiesCommitTimeFormat;
- }
-
- /**
- * Asynchronously searches the given jar file for a git.properties file.
- */
- @Override
- public void searchFileForGitPropertiesAsync(File file, boolean isJarFile) {
- executor.execute(() -> searchFile(file, isJarFile));
- }
-
- private void searchFile(File file, boolean isJarFile) {
- logger.debug("Searching jar file {} for a single git.properties", file);
- try {
- List data = dataExtractor.extractData(file, isJarFile, recursiveSearch, gitPropertiesCommitTimeFormat);
- if (data.isEmpty()) {
- logger.debug("No git.properties files found in {}", file.toString());
- return;
- }
- if (data.size() > 1) {
- logger.warn("Multiple git.properties files found in {}", file.toString() +
- ". Using the first one: " + data.get(0));
-
- }
- T dataEntry = data.get(0);
-
- if (foundData != null) {
- if (!foundData.equals(dataEntry)) {
- logger.warn(
- "Found inconsistent git.properties files: {} contained data {} while {} contained {}." +
- " Please ensure that all git.properties files of your application are consistent." +
- " Otherwise, you may" +
- " be uploading to the wrong project/commit which will result in incorrect coverage data" +
- " displayed in Teamscale. If you cannot fix the inconsistency, you can manually" +
- " specify a Jar/War/Ear/... file from which to read the correct git.properties" +
- " file with the agent's teamscale-git-properties-jar parameter.",
- jarFileWithGitProperties, foundData, file, data);
- }
- return;
- }
-
- logger.debug("Found git.properties file in {} and found commit descriptor {}", file.toString(),
- dataEntry);
- foundData = dataEntry;
- jarFileWithGitProperties = file;
- uploader.setCommitAndTriggerAsynchronousUpload(dataEntry);
- } catch (IOException | InvalidGitPropertiesException e) {
- logger.error("Error during asynchronous search for git.properties in {}", file.toString(), e);
- }
- }
-
- /** Functional interface for data extraction from a jar file. */
- @FunctionalInterface
- public interface DataExtractor {
- /** Extracts data from the JAR. */
- List extractData(File file, boolean isJarFile,
- boolean recursiveSearch,
- @Nullable DateTimeFormatter gitPropertiesCommitTimeFormat) throws IOException, InvalidGitPropertiesException;
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/InvalidGitPropertiesException.java b/agent/src/main/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/InvalidGitPropertiesException.java
deleted file mode 100644
index d2491d43c..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/InvalidGitPropertiesException.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.teamscale.jacoco.agent.commit_resolution.git_properties;
-
-/**
- * Thrown in case a git.properties file is found but it is malformed.
- */
-public class InvalidGitPropertiesException extends Exception {
- /*package*/ InvalidGitPropertiesException(String s, Throwable throwable) {
- super(s, throwable);
- }
-
- public InvalidGitPropertiesException(String s) {
- super(s);
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/commit_resolution/sapnwdi/NwdiMarkerClassLocatingTransformer.java b/agent/src/main/java/com/teamscale/jacoco/agent/commit_resolution/sapnwdi/NwdiMarkerClassLocatingTransformer.java
deleted file mode 100644
index 87d04b6f8..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/commit_resolution/sapnwdi/NwdiMarkerClassLocatingTransformer.java
+++ /dev/null
@@ -1,92 +0,0 @@
-package com.teamscale.jacoco.agent.commit_resolution.sapnwdi;
-
-import com.teamscale.client.CommitDescriptor;
-import com.teamscale.client.StringUtils;
-import com.teamscale.jacoco.agent.logging.LoggingUtils;
-import com.teamscale.jacoco.agent.options.sapnwdi.DelayedSapNwdiMultiUploader;
-import com.teamscale.jacoco.agent.options.sapnwdi.SapNwdiApplication;
-import com.teamscale.report.util.ClasspathWildcardIncludeFilter;
-import org.slf4j.Logger;
-
-import java.lang.instrument.ClassFileTransformer;
-import java.net.URL;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.attribute.BasicFileAttributes;
-import java.security.CodeSource;
-import java.security.ProtectionDomain;
-import java.util.Collection;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-/**
- * {@link ClassFileTransformer} that doesn't change the loaded classes but guesses the rough commit timestamp by
- * inspecting the last modification date of the applications marker class file.
- */
-public class NwdiMarkerClassLocatingTransformer implements ClassFileTransformer {
-
- /** The Design time repository-git-bridge (DTR-bridge) currently only exports a single branch named master. */
- private static final String DTR_BRIDGE_DEFAULT_BRANCH = "master";
- private final Logger logger = LoggingUtils.getLogger(this);
- private final DelayedSapNwdiMultiUploader store;
- private final ClasspathWildcardIncludeFilter locationIncludeFilter;
- private final Map markerClassesToApplications;
-
- public NwdiMarkerClassLocatingTransformer(
- DelayedSapNwdiMultiUploader store,
- ClasspathWildcardIncludeFilter locationIncludeFilter,
- Collection apps) {
- this.store = store;
- this.locationIncludeFilter = locationIncludeFilter;
- this.markerClassesToApplications = apps.stream().collect(
- Collectors.toMap(sapNwdiApplication -> sapNwdiApplication.getMarkerClass().replace('.', '/'),
- application -> application));
- }
-
- @Override
- public byte[] transform(ClassLoader classLoader, String className, Class> aClass,
- ProtectionDomain protectionDomain, byte[] classFileContent) {
- if (protectionDomain == null) {
- // happens for e.g. java.lang. We can ignore these classes
- return null;
- }
-
- if (StringUtils.isEmpty(className) || !locationIncludeFilter.isIncluded(className)) {
- // only search in jar files of included classes
- return null;
- }
-
- if (!this.markerClassesToApplications.containsKey(className)) {
- // only kick off search if the marker class was found.
- return null;
- }
-
- try {
- CodeSource codeSource = protectionDomain.getCodeSource();
- if (codeSource == null) {
- // unknown when this can happen, we suspect when code is generated at runtime
- // but there's nothing else we can do here in either case
- return null;
- }
-
- URL jarOrClassFolderUrl = codeSource.getLocation();
- logger.debug("Found " + className + " in " + jarOrClassFolderUrl);
-
- if (jarOrClassFolderUrl.getProtocol().equalsIgnoreCase("file")) {
- Path file = Paths.get(jarOrClassFolderUrl.toURI());
- BasicFileAttributes attr = Files.readAttributes(file, BasicFileAttributes.class);
- SapNwdiApplication application = markerClassesToApplications.get(className);
- CommitDescriptor commitDescriptor = new CommitDescriptor(
- DTR_BRIDGE_DEFAULT_BRANCH, attr.lastModifiedTime().toMillis());
- store.setCommitForApplication(commitDescriptor, application);
- }
- } catch (Throwable e) {
- // 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);
- }
- return null;
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/configuration/AgentOptionReceiveException.java b/agent/src/main/java/com/teamscale/jacoco/agent/configuration/AgentOptionReceiveException.java
deleted file mode 100644
index 80f9bb19d..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/configuration/AgentOptionReceiveException.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.teamscale.jacoco.agent.configuration;
-
-/** Thrown when retrieving the profiler configuration from Teamscale fails. */
-public class AgentOptionReceiveException extends Exception {
-
- /**
- * Serialization ID.
- */
- private static final long serialVersionUID = 1L;
-
- /**
- * Constructor.
- */
- public AgentOptionReceiveException(String message) {
- super(message);
- }
-
- /**
- * Constructor.
- */
- public AgentOptionReceiveException(String message, Throwable cause) {
- super(message, cause);
- }
-
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.java b/agent/src/main/java/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.java
deleted file mode 100644
index d4e59a7e6..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.java
+++ /dev/null
@@ -1,168 +0,0 @@
-package com.teamscale.jacoco.agent.configuration;
-
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.teamscale.client.ITeamscaleService;
-import com.teamscale.client.JsonUtils;
-import com.teamscale.client.ProcessInformation;
-import com.teamscale.client.ProfilerConfiguration;
-import com.teamscale.client.ProfilerInfo;
-import com.teamscale.client.ProfilerRegistration;
-import com.teamscale.client.TeamscaleServiceGenerator;
-import com.teamscale.jacoco.agent.logging.LoggingUtils;
-import com.teamscale.jacoco.agent.util.AgentUtils;
-import com.teamscale.report.util.ILogger;
-import okhttp3.HttpUrl;
-import okhttp3.ResponseBody;
-import org.jetbrains.annotations.NotNull;
-import retrofit2.Response;
-
-import java.io.IOException;
-import java.time.Duration;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Responsible for holding the configuration that was retrieved from Teamscale and sending regular heartbeat events to
- * keep the profiler information in Teamscale up to date.
- */
-public class ConfigurationViaTeamscale {
-
- /**
- * Two minute timeout. This is quite high to account for an eventual high load on the Teamscale server. This is a
- * tradeoff between fast application startup and potentially missing test coverage.
- */
- private static final Duration LONG_TIMEOUT = Duration.ofMinutes(2);
-
- /**
- * The UUID that Teamscale assigned to this instance of the profiler during the registration. This ID needs to be
- * used when communicating with Teamscale.
- */
- private final String profilerId;
-
- private final ITeamscaleService teamscaleClient;
- private final ProfilerInfo profilerInfo;
-
- public ConfigurationViaTeamscale(ITeamscaleService teamscaleClient, ProfilerRegistration profilerRegistration,
- ProcessInformation processInformation) {
- this.teamscaleClient = teamscaleClient;
- this.profilerId = profilerRegistration.profilerId;
- this.profilerInfo = new ProfilerInfo(processInformation, profilerRegistration.profilerConfiguration);
- }
-
- /**
- * Tries to retrieve the profiler configuration from Teamscale. In case retrieval fails the method throws a
- * {@link AgentOptionReceiveException}.
- */
- public static @NotNull ConfigurationViaTeamscale retrieve(ILogger logger, String configurationId, HttpUrl url,
- String userName,
- String userAccessToken) throws AgentOptionReceiveException {
- ITeamscaleService teamscaleClient = TeamscaleServiceGenerator
- .createService(ITeamscaleService.class, url, userName, userAccessToken, AgentUtils.USER_AGENT,
- LONG_TIMEOUT, LONG_TIMEOUT);
- try {
- ProcessInformation processInformation = new ProcessInformationRetriever(logger).getProcessInformation();
- Response response = teamscaleClient.registerProfiler(configurationId,
- processInformation).execute();
- if (!response.isSuccessful()) {
- throw new AgentOptionReceiveException(
- "Failed to retrieve profiler configuration from Teamscale due to failed request. Http status: " + response.code()
- + " Body: " + response.errorBody().string());
- }
-
- ResponseBody body = response.body();
- return parseProfilerRegistration(body, response, teamscaleClient, processInformation);
- } catch (IOException e) {
- // 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 new AgentOptionReceiveException(
- "Failed to retrieve profiler configuration from Teamscale due to network error: " + LoggingUtils.getStackTraceAsString(
- e),
- e);
- }
- }
-
- private static @NotNull ConfigurationViaTeamscale parseProfilerRegistration(ResponseBody body,
- Response response, ITeamscaleService teamscaleClient,
- ProcessInformation processInformation) throws AgentOptionReceiveException, IOException {
- if (body == null) {
- throw new AgentOptionReceiveException(
- "Failed to retrieve profiler configuration from Teamscale due to empty response. HTTP code: " + response.code());
- }
- // We may only call this once
- String bodyString = body.string();
- try {
- ProfilerRegistration registration = JsonUtils.deserialize(bodyString,
- ProfilerRegistration.class);
- if (registration == null) {
- throw new AgentOptionReceiveException(
- "Failed to retrieve profiler configuration from Teamscale due to invalid JSON. HTTP code: " + response.code() + " Response: " + bodyString);
- }
- return new ConfigurationViaTeamscale(teamscaleClient, registration, processInformation);
- } catch (JsonProcessingException e) {
- throw new AgentOptionReceiveException(
- "Failed to retrieve profiler configuration from Teamscale due to invalid JSON. HTTP code: " + response.code() + " Response: " + bodyString,
- e);
- }
- }
-
- /** Returns the profiler configuration that was retrieved from Teamscale. */
- public ProfilerConfiguration getProfilerConfiguration() {
- return profilerInfo.profilerConfiguration;
- }
-
-
- /**
- * Starts a heartbeat thread and registers a shutdown hook.
- *
- * This spawns a new thread every minute which sends a heartbeat to Teamscale. It also registers a shutdown hook
- * that unregisters the profiler from Teamscale.
- */
- public void startHeartbeatThreadAndRegisterShutdownHook() {
- ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(runnable -> {
- Thread thread = new Thread(runnable);
- thread.setDaemon(true);
- return thread;
- });
-
- executor.scheduleAtFixedRate(this::sendHeartbeat, 1, 1, TimeUnit.MINUTES);
-
- Runtime.getRuntime().addShutdownHook(new Thread(() -> {
- executor.shutdownNow();
- unregisterProfiler();
- }));
- }
-
- private void sendHeartbeat() {
- try {
- Response response = teamscaleClient.sendHeartbeat(profilerId, profilerInfo).execute();
- if (!response.isSuccessful()) {
- LoggingUtils.getLogger(this)
- .error("Failed to send heartbeat. Teamscale responded with: " + response.errorBody().string());
- }
- } catch (IOException e) {
- LoggingUtils.getLogger(this).error("Failed to send heartbeat to Teamscale!", e);
- }
- }
-
- /** Unregisters the profiler in Teamscale (marks it as shut down). */
- public void unregisterProfiler() {
- try {
- Response response = teamscaleClient.unregisterProfiler(profilerId).execute();
- if (response.code() == 405) {
- response = teamscaleClient.unregisterProfilerLegacy(profilerId).execute();
- }
- if (!response.isSuccessful()) {
- LoggingUtils.getLogger(this)
- .error("Failed to unregister profiler. Teamscale responded with: " + response.errorBody()
- .string());
- }
- } catch (IOException e) {
- LoggingUtils.getLogger(this).error("Failed to unregister profiler!", e);
- }
- }
-
- public String getProfilerId() {
- return profilerId;
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/configuration/ProcessInformationRetriever.java b/agent/src/main/java/com/teamscale/jacoco/agent/configuration/ProcessInformationRetriever.java
deleted file mode 100644
index 63a34f4cf..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/configuration/ProcessInformationRetriever.java
+++ /dev/null
@@ -1,64 +0,0 @@
-package com.teamscale.jacoco.agent.configuration;
-
-import com.teamscale.client.ProcessInformation;
-import com.teamscale.report.util.ILogger;
-
-import java.lang.management.ManagementFactory;
-import java.lang.reflect.InvocationTargetException;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
-
-/**
- * Is responsible for retrieving process information such as the host name and process ID.
- */
-public class ProcessInformationRetriever {
-
- private final ILogger logger;
-
- public ProcessInformationRetriever(ILogger logger) {
- this.logger = logger;
- }
-
- /**
- * Retrieves the process information, including the host name and process ID.
- */
- public ProcessInformation getProcessInformation() {
- String hostName = getHostName();
- String processId = getPID();
- return new ProcessInformation(hostName, processId, System.currentTimeMillis());
- }
-
- /**
- * Retrieves the host name of the local machine.
- */
- private String getHostName() {
- try {
- InetAddress inetAddress = InetAddress.getLocalHost();
- return inetAddress.getHostName();
- } catch (UnknownHostException e) {
- logger.error("Failed to determine hostname!", e);
- return "";
- }
- }
-
- /**
- * Returns a string that probably contains the PID.
- *
- * On Java 9 there is an API to get the PID. But since we support Java 8, we may fall back to an undocumented API
- * that at least contains the PID in most JVMs.
- *
- * See This
- * StackOverflow question
- */
- public static String getPID() {
- try {
- Class> processHandleClass = Class.forName("java.lang.ProcessHandle");
- Object processHandle = processHandleClass.getMethod("current").invoke(null);
- Long pid = (Long) processHandleClass.getMethod("pid").invoke(processHandle);
- return pid.toString();
- } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException |
- InvocationTargetException e) {
- return ManagementFactory.getRuntimeMXBean().getName();
- }
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/convert/ConvertCommand.java b/agent/src/main/java/com/teamscale/jacoco/agent/convert/ConvertCommand.java
deleted file mode 100644
index 116c5fe44..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/convert/ConvertCommand.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/*-------------------------------------------------------------------------+
-| |
-| Copyright (c) 2009-2017 CQSE GmbH |
-| |
-+-------------------------------------------------------------------------*/
-package com.teamscale.jacoco.agent.convert;
-
-import com.beust.jcommander.JCommander;
-import com.beust.jcommander.Parameter;
-import com.beust.jcommander.Parameters;
-import com.teamscale.client.FileSystemUtils;
-import com.teamscale.client.StringUtils;
-import com.teamscale.jacoco.agent.commandline.ICommand;
-import com.teamscale.jacoco.agent.commandline.Validator;
-import com.teamscale.jacoco.agent.options.ClasspathUtils;
-import com.teamscale.jacoco.agent.options.FilePatternResolver;
-import com.teamscale.jacoco.agent.util.Assertions;
-import com.teamscale.report.EDuplicateClassFileBehavior;
-import com.teamscale.report.util.CommandLineLogger;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.stream.Collectors;
-
-/**
- * Encapsulates all command line options for the convert command for parsing with {@link JCommander}.
- */
-@Parameters(commandNames = "convert", commandDescription = "Converts a binary .exec coverage file to XML. " +
- "Note that the XML report will only contain source file coverage information, but no class coverage.")
-public class ConvertCommand implements ICommand {
-
- /** The directories and/or zips that contain all class files being profiled. */
- @Parameter(names = {"--class-dir", "--jar", "-c"}, required = true, description = ""
- + "The directories or zip/ear/jar/war/... files that contain the compiled Java classes being profiled."
- + " Searches recursively, including inside zips. You may also supply a *.txt file with one path per line.")
- /* package */ List classDirectoriesOrZips = new ArrayList<>();
-
- /**
- * Wildcard include patterns to apply during JaCoCo's traversal of class files.
- */
- @Parameter(names = {"--includes"}, description = ""
- + "Wildcard include patterns to apply to all found class file locations during JaCoCo's traversal of class files."
- + " Note that zip contents are separated from zip files with @ and that you can filter only"
- + " class files, not intermediate folders/zips. Use with great care as missing class files"
- + " lead to broken coverage files! Turn on debug logging to see which locations are being filtered."
- + " Defaults to no filtering. Excludes overrule includes.")
- /* package */ List locationIncludeFilters = new ArrayList<>();
-
- /**
- * Wildcard exclude patterns to apply during JaCoCo's traversal of class files.
- */
- @Parameter(names = {"--excludes", "-e"}, description = ""
- + "Wildcard exclude patterns to apply to all found class file locations during JaCoCo's traversal of class files."
- + " Note that zip contents are separated from zip files with @ and that you can filter only"
- + " class files, not intermediate folders/zips. Use with great care as missing class files"
- + " lead to broken coverage files! Turn on debug logging to see which locations are being filtered."
- + " Defaults to no filtering. Excludes overrule includes.")
- /* package */ List locationExcludeFilters = new ArrayList<>();
-
- /** The directory to write the XML traces to. */
- @Parameter(names = {"--in", "-i"}, required = true, description = "" + "The binary .exec file(s), test details and " +
- "test executions to read. Can be a single file or a directory that is recursively scanned for relevant files.")
- /* package */ List inputFiles = new ArrayList<>();
-
- /** The directory to write the XML traces to. */
- @Parameter(names = {"--out", "-o"}, required = true, description = ""
- + "The file to write the generated XML report to.")
- /* package */ String outputFile = "";
-
- /** Whether to ignore duplicate, non-identical class files. */
- @Parameter(names = {"--duplicates", "-d"}, arity = 1, description = ""
- + "Whether to ignore duplicate, non-identical class files."
- + " This is discouraged and may result in incorrect coverage files. Defaults to WARN. " +
- "Options are FAIL, WARN and IGNORE.")
- /* package */ EDuplicateClassFileBehavior duplicateClassFileBehavior = EDuplicateClassFileBehavior.WARN;
-
- /** Whether to ignore uncovered class files. */
- @Parameter(names = {"--ignore-uncovered-classes"}, required = false, arity = 1, description = ""
- + "Whether to ignore uncovered classes."
- + " These classes will not be part of the XML report at all, making it considerably smaller in some cases. Defaults to false.")
- /* package */ boolean shouldIgnoreUncoveredClasses = false;
-
- /** Whether testwise coverage or jacoco coverage should be generated. */
- @Parameter(names = {"--testwise-coverage", "-t"}, required = false, arity = 0, description = "Whether testwise " +
- "coverage or jacoco coverage should be generated.")
- /* package */ boolean shouldGenerateTestwiseCoverage = false;
-
- /** After how many tests testwise coverage should be split into multiple reports. */
- @Parameter(names = {"--split-after", "-s"}, required = false, arity = 1, description = "After how many tests " +
- "testwise coverage should be split into multiple reports (Default is 5000).")
- private int splitAfter = 5000;
-
- /** @see #classDirectoriesOrZips */
- public List getClassDirectoriesOrZips() throws IOException {
- return ClasspathUtils
- .resolveClasspathTextFiles("class-dir", new FilePatternResolver(new CommandLineLogger()),
- classDirectoriesOrZips);
- }
-
- /** @see #locationIncludeFilters */
- public List getLocationIncludeFilters() {
- return locationIncludeFilters;
- }
-
- /** @see #locationExcludeFilters */
- public List getLocationExcludeFilters() {
- return locationExcludeFilters;
- }
-
- /** @see #inputFiles */
- public List getInputFiles() {
- return inputFiles.stream().map(File::new).collect(Collectors.toList());
- }
-
- /** @see #outputFile */
- public File getOutputFile() {
- return new File(outputFile);
- }
-
- /** @see #splitAfter */
- public int getSplitAfter() {
- return splitAfter;
- }
-
- /** @see #duplicateClassFileBehavior */
- public EDuplicateClassFileBehavior getDuplicateClassFileBehavior() {
- return duplicateClassFileBehavior;
- }
-
- /** Makes sure the arguments are valid. */
- @Override
- public Validator validate() {
- Validator validator = new Validator();
-
- List classDirectoriesOrZips = new ArrayList<>();
- validator.ensure(() -> classDirectoriesOrZips.addAll(getClassDirectoriesOrZips()));
- validator.isFalse(classDirectoriesOrZips.isEmpty(),
- "You must specify at least one directory or zip that contains class files");
- for (File path : classDirectoriesOrZips) {
- validator.isTrue(path.exists(), "Path '" + path + "' does not exist");
- validator.isTrue(path.canRead(), "Path '" + path + "' is not readable");
- }
-
- for (File inputFile : getInputFiles()) {
- validator.isTrue(inputFile.exists() && inputFile.canRead(),
- "Cannot read the input file " + inputFile);
- }
-
- validator.ensure(() -> {
- Assertions.isFalse(StringUtils.isEmpty(outputFile), "You must specify an output file");
- File outputDir = getOutputFile().getAbsoluteFile().getParentFile();
- FileSystemUtils.ensureDirectoryExists(outputDir);
- Assertions.isTrue(outputDir.canWrite(), "Path '" + outputDir + "' is not writable");
- });
-
- return validator;
- }
-
- /** {@inheritDoc} */
- @Override
- public void run() throws Exception {
- Converter converter = new Converter(this);
- if (this.shouldGenerateTestwiseCoverage) {
- converter.runTestwiseCoverageReportGeneration();
- } else {
- converter.runJaCoCoReportGeneration();
- }
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/convert/Converter.java b/agent/src/main/java/com/teamscale/jacoco/agent/convert/Converter.java
deleted file mode 100644
index e46cc5852..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/convert/Converter.java
+++ /dev/null
@@ -1,95 +0,0 @@
-package com.teamscale.jacoco.agent.convert;
-
-import com.teamscale.client.TestDetails;
-import com.teamscale.jacoco.agent.logging.LoggingUtils;
-import com.teamscale.jacoco.agent.options.AgentOptionParseException;
-import com.teamscale.jacoco.agent.util.Benchmark;
-import com.teamscale.report.ReportUtils;
-import com.teamscale.report.jacoco.EmptyReportException;
-import com.teamscale.report.jacoco.JaCoCoXmlReportGenerator;
-import com.teamscale.report.testwise.ETestArtifactFormat;
-import com.teamscale.report.testwise.TestwiseCoverageReportWriter;
-import com.teamscale.report.testwise.jacoco.JaCoCoTestwiseReportGenerator;
-import com.teamscale.report.testwise.model.TestExecution;
-import com.teamscale.report.testwise.model.factory.TestInfoFactory;
-import com.teamscale.report.util.ClasspathWildcardIncludeFilter;
-import com.teamscale.report.util.CommandLineLogger;
-import com.teamscale.report.util.ILogger;
-import org.slf4j.Logger;
-
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Paths;
-import java.util.List;
-
-import static com.teamscale.jacoco.agent.logging.LoggingUtils.wrap;
-
-/** Converts one .exec binary coverage file to XML. */
-public class Converter {
-
- /** The command line arguments. */
- private ConvertCommand arguments;
-
- /** Constructor. */
- public Converter(ConvertCommand arguments) {
- this.arguments = arguments;
- }
-
- /** Converts one .exec binary coverage file to XML. */
- public void runJaCoCoReportGeneration() throws IOException {
- List jacocoExecutionDataList = ReportUtils
- .listFiles(ETestArtifactFormat.JACOCO, arguments.getInputFiles());
-
- Logger logger = LoggingUtils.getLogger(this);
- JaCoCoXmlReportGenerator generator = new JaCoCoXmlReportGenerator(arguments.getClassDirectoriesOrZips(),
- getWildcardIncludeExcludeFilter(), arguments.getDuplicateClassFileBehavior(),
- arguments.shouldIgnoreUncoveredClasses,
- wrap(logger));
-
- try (Benchmark benchmark = new Benchmark("Generating the XML report")) {
- generator.convertExecFilesToReport(jacocoExecutionDataList, Paths.get(arguments.outputFile).toFile());
- } catch (EmptyReportException e) {
- logger.warn("Converted report was empty.", e);
- }
- }
-
- /** Converts one .exec binary coverage file, test details and test execution files to JSON testwise coverage. */
- public void runTestwiseCoverageReportGeneration() throws IOException, AgentOptionParseException {
- List testDetails = ReportUtils.readObjects(ETestArtifactFormat.TEST_LIST,
- TestDetails[].class, arguments.getInputFiles());
- List testExecutions = ReportUtils.readObjects(ETestArtifactFormat.TEST_EXECUTION,
- TestExecution[].class, arguments.getInputFiles());
-
- List jacocoExecutionDataList = ReportUtils
- .listFiles(ETestArtifactFormat.JACOCO, arguments.getInputFiles());
- ILogger logger = new CommandLineLogger();
-
- JaCoCoTestwiseReportGenerator generator = new JaCoCoTestwiseReportGenerator(
- arguments.getClassDirectoriesOrZips(),
- getWildcardIncludeExcludeFilter(),
- arguments.getDuplicateClassFileBehavior(),
- logger
- );
-
- TestInfoFactory testInfoFactory = new TestInfoFactory(testDetails, testExecutions);
-
- try (Benchmark benchmark = new Benchmark("Generating the testwise coverage report")) {
- logger.info(
- "Writing report with " + testDetails.size() + " Details/" + testExecutions.size() + " Results");
-
- try (TestwiseCoverageReportWriter coverageWriter = new TestwiseCoverageReportWriter(testInfoFactory,
- arguments.getOutputFile(), arguments.getSplitAfter(), null)) {
- for (File executionDataFile : jacocoExecutionDataList) {
- generator.convertAndConsume(executionDataFile, coverageWriter);
- }
- }
- }
- }
-
- private ClasspathWildcardIncludeFilter getWildcardIncludeExcludeFilter() {
- return new ClasspathWildcardIncludeFilter(
- String.join(":", arguments.getLocationIncludeFilters()),
- String.join(":", arguments.getLocationExcludeFilters()));
- }
-
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/logging/DebugLogDirectoryPropertyDefiner.java b/agent/src/main/java/com/teamscale/jacoco/agent/logging/DebugLogDirectoryPropertyDefiner.java
deleted file mode 100644
index ce83d7422..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/logging/DebugLogDirectoryPropertyDefiner.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.teamscale.jacoco.agent.logging;
-
-import java.nio.file.Path;
-
-/** Defines a property that contains the path to which log files should be written. */
-public class DebugLogDirectoryPropertyDefiner extends LogDirectoryPropertyDefiner {
-
- /** File path for debug logging. */
- /* package */ static Path filePath = null;
-
- @Override
- public String getPropertyValue() {
- if (filePath == null) {
- return super.getPropertyValue();
- }
- return filePath.resolve("logs").toAbsolutePath().toString();
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogDirectoryPropertyDefiner.java b/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogDirectoryPropertyDefiner.java
deleted file mode 100644
index a10c57221..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogDirectoryPropertyDefiner.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.teamscale.jacoco.agent.logging;
-
-import ch.qos.logback.core.PropertyDefinerBase;
-import com.teamscale.jacoco.agent.util.AgentUtils;
-
-import java.nio.file.Path;
-
-/** Defines a property that contains the default path to which log files should be written. */
-public class LogDirectoryPropertyDefiner extends PropertyDefinerBase {
- @Override
- public String getPropertyValue() {
- Path tempDirectory = AgentUtils.getMainTempDirectory();
- return tempDirectory.resolve("logs").toAbsolutePath().toString();
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java b/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java
deleted file mode 100644
index 4a1906388..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java
+++ /dev/null
@@ -1,207 +0,0 @@
-package com.teamscale.jacoco.agent.logging;
-
-import ch.qos.logback.classic.Logger;
-import ch.qos.logback.classic.LoggerContext;
-import ch.qos.logback.classic.spi.ILoggingEvent;
-import ch.qos.logback.core.AppenderBase;
-import ch.qos.logback.core.status.ErrorStatus;
-import com.teamscale.client.ITeamscaleService;
-import com.teamscale.client.ProfilerLogEntry;
-import com.teamscale.client.TeamscaleClient;
-import com.teamscale.jacoco.agent.options.AgentOptions;
-import org.jetbrains.annotations.Nullable;
-import retrofit2.Call;
-
-import java.net.ConnectException;
-import java.time.Duration;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.IdentityHashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import static com.teamscale.jacoco.agent.logging.LoggingUtils.getStackTraceFromEvent;
-
-/**
- * Custom log appender that sends logs to Teamscale; it buffers log that were not sent due to connection issues and
- * sends them later.
- */
-public class LogToTeamscaleAppender extends AppenderBase {
-
- /** Flush the logs after N elements are in the queue */
- private static final int BATCH_SIZE = 50;
-
- /** Flush the logs in the given time interval */
- private static final Duration FLUSH_INTERVAL = Duration.ofSeconds(3);
-
- /** The unique ID of the profiler */
- private String profilerId;
-
- /** The service client for sending logs to Teamscale */
- private static ITeamscaleService teamscaleClient;
-
- /**
- * Buffer for unsent logs. We use a set here to allow for removing entries fast after sending them to Teamscale was
- * successful.
- */
- private final LinkedHashSet logBuffer = new LinkedHashSet<>();
-
- /** Scheduler for sending logs after the configured time interval */
- private final ScheduledExecutorService scheduler;
-
- /** Active log flushing threads */
- private final Set> activeLogFlushes = Collections.newSetFromMap(new IdentityHashMap<>());
-
- /** Is there a flush going on right now? */
- private final AtomicBoolean isFlusing = new AtomicBoolean(false);
-
- public LogToTeamscaleAppender() {
- this.scheduler = Executors.newScheduledThreadPool(1, r -> {
- // Make the thread a daemon so that it does not prevent the JVM from terminating.
- Thread t = Executors.defaultThreadFactory().newThread(r);
- t.setDaemon(true);
- return t;
- });
- }
-
- @Override
- public void start() {
- super.start();
- scheduler.scheduleAtFixedRate(() -> {
- synchronized (activeLogFlushes) {
- activeLogFlushes.removeIf(CompletableFuture::isDone);
- if (this.activeLogFlushes.isEmpty()) {
- flush();
- }
- }
- }, FLUSH_INTERVAL.toMillis(), FLUSH_INTERVAL.toMillis(), TimeUnit.MILLISECONDS);
- }
-
- @Override
- protected void append(ILoggingEvent eventObject) {
- synchronized (logBuffer) {
- logBuffer.add(formatLog(eventObject));
- if (logBuffer.size() >= BATCH_SIZE) {
- flush();
- }
- }
- }
-
- private ProfilerLogEntry formatLog(ILoggingEvent eventObject) {
- String trace = getStackTraceFromEvent(eventObject);
- long timestamp = eventObject.getTimeStamp();
- String message = eventObject.getFormattedMessage();
- String severity = eventObject.getLevel().toString();
- return new ProfilerLogEntry(timestamp, message, trace, severity);
- }
-
- private void flush() {
- sendLogs();
- }
-
- /** Send logs in a separate thread */
- private void sendLogs() {
- synchronized (activeLogFlushes) {
- activeLogFlushes.add(CompletableFuture.runAsync(() -> {
- if (isFlusing.compareAndSet(false, true)) {
- try {
- if (teamscaleClient == null) {
- // There might be no connection configured.
- return;
- }
-
- List logsToSend;
- synchronized (logBuffer) {
- logsToSend = new ArrayList<>(logBuffer);
- }
-
- Call call = teamscaleClient.postProfilerLog(profilerId, logsToSend);
- retrofit2.Response response = call.execute();
- if (!response.isSuccessful()) {
- throw new IllegalStateException("Failed to send log: HTTP error code : " + response.code());
- }
-
- synchronized (logBuffer) {
- // Removing the logs that have been sent after the fact.
- // This handles problems with lost network connections.
- logsToSend.forEach(logBuffer::remove);
- }
- } catch (Exception e) {
- // We do not report on exceptions here.
- if (!(e instanceof ConnectException)) {
- addStatus(new ErrorStatus("Sending logs to Teamscale failed: " + e.getMessage(), this, e));
- }
- } finally {
- isFlusing.set(false);
- }
- }
- }).whenComplete((result, throwable) -> {
- synchronized (activeLogFlushes) {
- activeLogFlushes.removeIf(CompletableFuture::isDone);
- }
- }));
- }
- }
-
- @Override
- public void stop() {
- // Already flush here once to make sure that we do not miss too much.
- flush();
-
- scheduler.shutdown();
- try {
- if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
- scheduler.shutdownNow();
- }
- } catch (InterruptedException e) {
- scheduler.shutdownNow();
- }
-
- // A final flush after the scheduler has been shut down.
- flush();
-
- // Block until all flushes are done
- CompletableFuture.allOf(activeLogFlushes.toArray(new CompletableFuture[0])).join();
-
- super.stop();
- }
-
- public void setTeamscaleClient(ITeamscaleService teamscaleClient) {
- this.teamscaleClient = teamscaleClient;
- }
-
- public void setProfilerId(String profilerId) {
- this.profilerId = profilerId;
- }
-
- /**
- * Add the {@link com.teamscale.jacoco.agent.logging.LogToTeamscaleAppender} to the logging configuration and
- * enable/start it.
- */
- public static boolean addTeamscaleAppenderTo(LoggerContext context, AgentOptions agentOptions) {
- @Nullable TeamscaleClient client = agentOptions.createTeamscaleClient(
- false);
- if (client == null || agentOptions.configurationViaTeamscale == null) {
- return false;
- }
-
- ITeamscaleService serviceClient = client.getService();
- LogToTeamscaleAppender logToTeamscaleAppender = new LogToTeamscaleAppender();
- logToTeamscaleAppender.setContext(context);
- logToTeamscaleAppender.setProfilerId(agentOptions.configurationViaTeamscale.getProfilerId());
- logToTeamscaleAppender.setTeamscaleClient(serviceClient);
- logToTeamscaleAppender.start();
-
- Logger rootLogger = context.getLogger(Logger.ROOT_LOGGER_NAME);
- rootLogger.addAppender(logToTeamscaleAppender);
-
- return true;
- }
-
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LoggingUtils.java b/agent/src/main/java/com/teamscale/jacoco/agent/logging/LoggingUtils.java
deleted file mode 100644
index d836d8e6f..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LoggingUtils.java
+++ /dev/null
@@ -1,172 +0,0 @@
-/*-------------------------------------------------------------------------+
-| |
-| Copyright (c) 2009-2018 CQSE GmbH |
-| |
-+-------------------------------------------------------------------------*/
-package com.teamscale.jacoco.agent.logging;
-
-import ch.qos.logback.classic.LoggerContext;
-import ch.qos.logback.classic.joran.JoranConfigurator;
-import ch.qos.logback.classic.spi.ILoggingEvent;
-import ch.qos.logback.classic.spi.IThrowableProxy;
-import ch.qos.logback.classic.spi.ThrowableProxy;
-import ch.qos.logback.classic.spi.ThrowableProxyUtil;
-import ch.qos.logback.core.joran.spi.JoranException;
-import ch.qos.logback.core.util.StatusPrinter;
-import com.teamscale.jacoco.agent.Agent;
-import com.teamscale.jacoco.agent.util.NullOutputStream;
-import com.teamscale.report.util.ILogger;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.PrintStream;
-import java.nio.file.Path;
-
-/**
- * Helps initialize the logging framework properly.
- */
-public class LoggingUtils {
-
- /** Returns a logger for the given object's class. */
- public static Logger getLogger(Object object) {
- return LoggerFactory.getLogger(object.getClass());
- }
-
- /** Returns a logger for the given class. */
- public static Logger getLogger(Class> object) {
- return LoggerFactory.getLogger(object);
- }
-
- /** Class to use with try-with-resources to close the logging framework's resources. */
- public static class LoggingResources implements AutoCloseable {
-
- @Override
- public void close() {
- getLoggerContext().stop();
- }
- }
-
- /** Initializes the logging to the default configured in the Jar. */
- public static LoggingResources initializeDefaultLogging() {
- InputStream stream = Agent.class.getResourceAsStream("logback-default.xml");
- reconfigureLoggerContext(stream);
- return new LoggingResources();
- }
-
- /**
- * Returns the logger context.
- */
- public static LoggerContext getLoggerContext() {
- return (LoggerContext) LoggerFactory.getILoggerFactory();
- }
-
- /**
- * Extracts the stack trace from an ILoggingEvent using ThrowableProxyUtil.
- *
- * @param event the logging event containing the exception
- * @return the stack trace as a String, or null if no exception is associated
- */
- public static String getStackTraceFromEvent(ILoggingEvent event) {
- IThrowableProxy throwableProxy = event.getThrowableProxy();
-
- if (throwableProxy != null) {
- // Use ThrowableProxyUtil to convert the IThrowableProxy to a String
- return ThrowableProxyUtil.asString(throwableProxy);
- }
-
- return null;
- }
-
- /**
- * Converts a Throwable to its stack trace as a String.
- *
- * @param throwable the throwable to convert
- * @return the stack trace as a String
- */
- public static String getStackTraceAsString(Throwable throwable) {
- if (throwable == null) {
- return null;
- }
- return ThrowableProxyUtil.asString(new ThrowableProxy(throwable));
- }
-
- /**
- * Reconfigures the logger context to use the configuration XML from the given input stream. Cf. https://logback.qos.ch/manual/configuration.html
- */
- private static void reconfigureLoggerContext(InputStream stream) {
- StatusPrinter.setPrintStream(new PrintStream(new NullOutputStream()));
- LoggerContext loggerContext = getLoggerContext();
- try {
- JoranConfigurator configurator = new JoranConfigurator();
- configurator.setContext(loggerContext);
- loggerContext.reset();
- configurator.doConfigure(stream);
- } catch (JoranException je) {
- // StatusPrinter will handle this
- }
- StatusPrinter.printInCaseOfErrorsOrWarnings(loggerContext);
- }
-
- /**
- * Initializes the logging from the given file. If that is null, uses {@link
- * #initializeDefaultLogging()} instead.
- */
- public static LoggingResources initializeLogging(Path loggingConfigFile) throws IOException {
- if (loggingConfigFile == null) {
- return initializeDefaultLogging();
- }
-
- reconfigureLoggerContext(new FileInputStream(loggingConfigFile.toFile()));
- return new LoggingResources();
- }
-
- /** Initializes debug logging. */
- public static LoggingResources initializeDebugLogging(Path logDirectory) {
- if (logDirectory != null) {
- DebugLogDirectoryPropertyDefiner.filePath = logDirectory;
- }
- InputStream stream = Agent.class.getResourceAsStream("logback-default-debugging.xml");
- reconfigureLoggerContext(stream);
- return new LoggingResources();
- }
-
- /** Wraps the given slf4j logger into an {@link ILogger}. */
- public static ILogger wrap(Logger logger) {
- return new ILogger() {
- @Override
- public void debug(String message) {
- logger.debug(message);
- }
-
- @Override
- public void info(String message) {
- logger.info(message);
- }
-
- @Override
- public void warn(String message) {
- logger.warn(message);
- }
-
- @Override
- public void warn(String message, Throwable throwable) {
- logger.warn(message, throwable);
- }
-
- @Override
- public void error(Throwable throwable) {
- logger.error(throwable.getMessage(), throwable);
- }
-
- @Override
- public void error(String message, Throwable throwable) {
- logger.error(message, throwable);
- }
- };
- }
-
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptions.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptions.java
index 4a19f3e3f..c34068750 100644
--- a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptions.java
+++ b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptions.java
@@ -559,17 +559,17 @@ private void registerSingleGitPropertiesLocator(DelayedUploader createDelayedSingleProjectTeamscaleUploader() {
return new DelayedUploader<>(
projectAndCommit -> {
- if (!StringUtils.isEmpty(projectAndCommit.getProject()) && !teamscaleServer.project
- .equals(projectAndCommit.getProject())) {
+ if (!StringUtils.isEmpty(projectAndCommit.project) && !teamscaleServer.project
+ .equals(projectAndCommit.project)) {
logger.warn(
- "Teamscale project '" + teamscaleServer.project + "' specified in the agent configuration is not the same as the Teamscale project '" + projectAndCommit.getProject() + "' specified in git.properties file(s). Proceeding to upload to the" +
+ "Teamscale project '" + teamscaleServer.project + "' specified in the agent configuration is not the same as the Teamscale project '" + projectAndCommit.project + "' specified in git.properties file(s). Proceeding to upload to the" +
" Teamscale project '" + teamscaleServer.project + "' specified in the agent configuration.");
}
- if (projectAndCommit.getCommitInfo().preferCommitDescriptorOverRevision ||
- StringUtils.isEmpty(projectAndCommit.getCommitInfo().revision)) {
- teamscaleServer.commit = projectAndCommit.getCommitInfo().commit;
+ if (projectAndCommit.commitInfo.preferCommitDescriptorOverRevision ||
+ StringUtils.isEmpty(projectAndCommit.commitInfo.revision)) {
+ teamscaleServer.commit = projectAndCommit.commitInfo.commit;
} else {
- teamscaleServer.revision = projectAndCommit.getCommitInfo().revision;
+ teamscaleServer.revision = projectAndCommit.commitInfo.revision;
}
return new TeamscaleUploader(teamscaleServer);
}, outputDirectory);
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/ProjectAndCommit.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/ProjectAndCommit.java
deleted file mode 100644
index 58e9d104c..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/options/ProjectAndCommit.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package com.teamscale.jacoco.agent.options;
-
-import com.teamscale.jacoco.agent.commit_resolution.git_properties.CommitInfo;
-
-import java.util.Objects;
-
-/** Class encapsulating the Teamscale project and git commitInfo an upload should be performed to. */
-public class ProjectAndCommit {
-
- private final String project;
- private final CommitInfo commitInfo;
-
- public ProjectAndCommit(String project, CommitInfo commitInfo) {
- this.project = project;
- this.commitInfo = commitInfo;
- }
-
- /** @see #project */
- public String getProject() {
- return project;
- }
-
- /** @see #commitInfo */
- public CommitInfo getCommitInfo() {
- return commitInfo;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
- ProjectAndCommit that = (ProjectAndCommit) o;
- return Objects.equals(project, that.project) &&
- Objects.equals(commitInfo, that.commitInfo);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(project, commitInfo);
- }
-
- @Override
- public String toString() {
- return "ProjectRevision{" +
- "project='" + project + '\'' +
- ", commitInfo='" + commitInfo + '\'' +
- '}';
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/upload/DelayedMultiUploaderBase.java b/agent/src/main/java/com/teamscale/jacoco/agent/upload/DelayedMultiUploaderBase.java
deleted file mode 100644
index 2e76f9a9f..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/upload/DelayedMultiUploaderBase.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package com.teamscale.jacoco.agent.upload;
-
-import java.util.Collection;
-import java.util.stream.Collectors;
-
-import org.slf4j.Logger;
-
-import com.teamscale.jacoco.agent.logging.LoggingUtils;
-import com.teamscale.report.jacoco.CoverageFile;
-
-/**
- * Base class for wrapper uploaders that allow uploading the same coverage to
- * multiple locations.
- */
-public abstract class DelayedMultiUploaderBase implements IUploader {
-
- /** Logger. */
- protected final Logger logger = LoggingUtils.getLogger(this);
-
- @Override
- public synchronized void upload(CoverageFile file) {
- Collection wrappedUploaders = getWrappedUploaders();
- wrappedUploaders.forEach(uploader -> file.acquireReference());
- if (wrappedUploaders.isEmpty()) {
- logger.warn("No commits have been found yet to which coverage should be uploaded. Discarding coverage");
- } else {
- for (IUploader wrappedUploader : wrappedUploaders) {
- wrappedUploader.upload(file);
- }
- }
- }
-
- @Override
- public String describe() {
- Collection wrappedUploaders = getWrappedUploaders();
- if (!wrappedUploaders.isEmpty()) {
- return wrappedUploaders.stream().map(IUploader::describe).collect(Collectors.joining(", "));
- }
- return "Temporary stand-in until commit is resolved";
- }
-
- /** Returns the actual uploaders that this multiuploader wraps. */
- protected abstract Collection getWrappedUploaders();
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/upload/HttpZipUploaderBase.java b/agent/src/main/java/com/teamscale/jacoco/agent/upload/HttpZipUploaderBase.java
deleted file mode 100644
index 3d36723cd..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/upload/HttpZipUploaderBase.java
+++ /dev/null
@@ -1,153 +0,0 @@
-package com.teamscale.jacoco.agent.upload;
-
-import com.teamscale.client.FileSystemUtils;
-import com.teamscale.client.HttpUtils;
-import com.teamscale.jacoco.agent.logging.LoggingUtils;
-import com.teamscale.jacoco.agent.util.Benchmark;
-import com.teamscale.report.jacoco.CoverageFile;
-import okhttp3.HttpUrl;
-import okhttp3.OkHttpClient;
-import okhttp3.ResponseBody;
-import org.slf4j.Logger;
-import retrofit2.Response;
-import retrofit2.Retrofit;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.List;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipOutputStream;
-
-/** Base class for uploading the coverage zip to a provided url */
-public abstract class HttpZipUploaderBase implements IUploader {
-
- /** The logger. */
- protected final Logger logger = LoggingUtils.getLogger(this);
-
- /** The URL to upload to. */
- protected HttpUrl uploadUrl;
-
- /** Additional files to include in the uploaded zip. */
- protected final List additionalMetaDataFiles;
-
- /** The API class. */
- private final Class apiClass;
-
- /** The API which performs the upload */
- private T api;
-
- /** Constructor. */
- public HttpZipUploaderBase(HttpUrl uploadUrl, List additionalMetaDataFiles, Class apiClass) {
- this.uploadUrl = uploadUrl;
- this.additionalMetaDataFiles = additionalMetaDataFiles;
- this.apiClass = apiClass;
- }
-
- /** Template method to configure the OkHttp Client. */
- protected void configureOkHttp(OkHttpClient.Builder builder) {
- }
-
- /** Returns the API for creating request to the http uploader */
- protected T getApi() {
- if (api == null) {
- Retrofit retrofit = HttpUtils.createRetrofit(retrofitBuilder -> retrofitBuilder.baseUrl(uploadUrl),
- this::configureOkHttp);
- api = retrofit.create(apiClass);
- }
-
- return api;
- }
-
- /** Uploads the coverage zip to the server */
- protected abstract Response uploadCoverageZip(File coverageFile)
- throws IOException, UploaderException;
-
- @Override
- public void upload(CoverageFile coverageFile) {
- try (Benchmark ignored = new Benchmark("Uploading report via HTTP")) {
- if (tryUpload(coverageFile)) {
- coverageFile.delete();
- } else {
- logger.warn("Failed to upload coverage to Teamscale. "
- + "Won't delete local file {} so that the upload can automatically be retried upon profiler restart. "
- + "Upload can also be retried manually.", coverageFile);
- if (this instanceof IUploadRetry) {
- ((IUploadRetry) this).markFileForUploadRetry(coverageFile);
- }
- }
- } catch (IOException e) {
- logger.warn("Could not delete file {} after upload", coverageFile);
- }
- }
-
- /** Performs the upload and returns true if successful. */
- protected boolean tryUpload(CoverageFile coverageFile) {
- logger.debug("Uploading coverage to {}", uploadUrl);
-
- File zipFile;
- try {
- zipFile = createZipFile(coverageFile);
- } catch (IOException e) {
- logger.error("Failed to compile coverage zip file for upload to {}", uploadUrl, e);
- return false;
- }
-
- try {
- Response response = uploadCoverageZip(zipFile);
- if (response.isSuccessful()) {
- return true;
- }
-
- String errorBody = "";
- if (response.errorBody() != null) {
- errorBody = response.errorBody().string();
- }
-
- logger.error("Failed to upload coverage to {}. Request failed with error code {}. Error:\n{}", uploadUrl,
- response.code(), errorBody);
- return false;
- } catch (IOException e) {
- logger.error("Failed to upload coverage to {}. Probably a network problem", uploadUrl, e);
- return false;
- } catch (UploaderException e) {
- logger.error("Failed to upload coverage to {}. The configuration is probably incorrect", uploadUrl, e);
- return false;
- } finally {
- zipFile.delete();
- }
- }
-
- /**
- * Creates the zip file in the system temp directory to upload which includes the given coverage XML and all
- * {@link #additionalMetaDataFiles}. The file is marked to be deleted on exit.
- */
- private File createZipFile(CoverageFile coverageFile) throws IOException {
- File zipFile = Files.createTempFile(coverageFile.getNameWithoutExtension(), ".zip").toFile();
- zipFile.deleteOnExit();
- try (FileOutputStream fileOutputStream = new FileOutputStream(zipFile);
- ZipOutputStream zipOutputStream = new ZipOutputStream(fileOutputStream)) {
- fillZipFile(zipOutputStream, coverageFile);
- return zipFile;
- }
- }
-
- /**
- * Fills the upload zip file with the given coverage XML and all {@link #additionalMetaDataFiles}.
- */
- private void fillZipFile(ZipOutputStream zipOutputStream, CoverageFile coverageFile) throws IOException {
- zipOutputStream.putNextEntry(new ZipEntry(getZipEntryCoverageFileName(coverageFile)));
- coverageFile.copyStream(zipOutputStream);
-
- for (Path additionalFile : additionalMetaDataFiles) {
- zipOutputStream.putNextEntry(new ZipEntry(additionalFile.getFileName().toString()));
- zipOutputStream.write(FileSystemUtils.readFileBinary(additionalFile.toFile()));
- }
- }
-
- protected String getZipEntryCoverageFileName(CoverageFile coverageFile) {
- return "coverage.xml";
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/upload/IUploader.java b/agent/src/main/java/com/teamscale/jacoco/agent/upload/IUploader.java
deleted file mode 100644
index 0cc1fdcb1..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/upload/IUploader.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.teamscale.jacoco.agent.upload;
-
-import com.teamscale.report.jacoco.CoverageFile;
-
-/** Uploads coverage reports. */
-public interface IUploader {
-
- /**
- * Uploads the given coverage file. If the upload was successful, the coverage
- * file on disk will be deleted. Otherwise the file is left on disk and a
- * warning is logged.
- */
- void upload(CoverageFile coverageFile);
-
- /** Human-readable description of the uploader. */
- String describe();
-
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/upload/LocalDiskUploader.java b/agent/src/main/java/com/teamscale/jacoco/agent/upload/LocalDiskUploader.java
deleted file mode 100644
index 67c01f3ae..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/upload/LocalDiskUploader.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.teamscale.jacoco.agent.upload;
-
-import com.teamscale.report.jacoco.CoverageFile;
-
-/**
- * Dummy uploader which keeps the coverage file written by the agent on disk,
- * but does not actually perform uploads.
- */
-public class LocalDiskUploader implements IUploader {
- @Override
- public void upload(CoverageFile coverageFile) {
- // Don't delete the file here. We want to store the file permanently on disk in
- // case no uploader is configured.
- }
-
- @Override
- public String describe() {
- return "configured output directory on the local disk";
- }
-
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/upload/UploaderException.java b/agent/src/main/java/com/teamscale/jacoco/agent/upload/UploaderException.java
deleted file mode 100644
index a022c25bd..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/upload/UploaderException.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.teamscale.jacoco.agent.upload;
-
-import okhttp3.ResponseBody;
-import retrofit2.Response;
-
-import java.io.IOException;
-
-/**
- * Exception thrown from an uploader. Either during the upload or in the validation process.
- */
-public class UploaderException extends Exception {
-
- /** Constructor */
- public UploaderException(String message, Exception e) {
- super(message, e);
- }
-
- /** Constructor */
- public UploaderException(String message) {
- super(message);
- }
-
- /** Constructor */
- public UploaderException(String message, Response response) {
- super(createResponseMessage(message, response));
- }
-
- private static String createResponseMessage(String message, Response response) {
- try {
- String errorBodyMessage = response.errorBody().string();
- return String.format("%s (%s): \n%s", message, response.code(), errorBodyMessage);
- } catch (IOException | NullPointerException e) {
- return message;
- }
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/upload/artifactory/ArtifactoryConfig.java b/agent/src/main/java/com/teamscale/jacoco/agent/upload/artifactory/ArtifactoryConfig.java
deleted file mode 100644
index fceead42b..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/upload/artifactory/ArtifactoryConfig.java
+++ /dev/null
@@ -1,177 +0,0 @@
-package com.teamscale.jacoco.agent.upload.artifactory;
-
-import com.teamscale.client.StringUtils;
-import com.teamscale.jacoco.agent.commit_resolution.git_properties.CommitInfo;
-import com.teamscale.jacoco.agent.commit_resolution.git_properties.GitPropertiesLocatorUtils;
-import com.teamscale.jacoco.agent.commit_resolution.git_properties.InvalidGitPropertiesException;
-import com.teamscale.jacoco.agent.options.AgentOptionParseException;
-import com.teamscale.jacoco.agent.options.AgentOptionsParser;
-import com.teamscale.jacoco.agent.upload.UploaderException;
-import okhttp3.HttpUrl;
-import org.jetbrains.annotations.Nullable;
-
-import java.io.File;
-import java.io.IOException;
-import java.time.format.DateTimeFormatter;
-import java.util.List;
-
-/** Config necessary to upload files to an azure file storage. */
-public class ArtifactoryConfig {
- /**
- * Option to specify the artifactory URL. This shall be the entire path down to the directory to which the coverage
- * should be uploaded to, not only the base url of artifactory.
- */
- public static final String ARTIFACTORY_URL_OPTION = "artifactory-url";
-
- /**
- * Username that shall be used for basic auth. Alternative to basic auth is to use an API key with the
- * {@link ArtifactoryConfig#ARTIFACTORY_API_KEY_OPTION}
- */
- public static final String ARTIFACTORY_USER_OPTION = "artifactory-user";
-
- /**
- * Password that shall be used for basic auth. Alternative to basic auth is to use an API key with the
- * {@link ArtifactoryConfig#ARTIFACTORY_API_KEY_OPTION}
- */
- public static final String ARTIFACTORY_PASSWORD_OPTION = "artifactory-password";
-
- /**
- * API key that shall be used to authenticate requests to artifactory with the
- * {@link com.teamscale.jacoco.agent.upload.artifactory.ArtifactoryUploader#ARTIFACTORY_API_HEADER}. Alternatively
- * basic auth with username ({@link ArtifactoryConfig#ARTIFACTORY_USER_OPTION}) and password
- * ({@link ArtifactoryConfig#ARTIFACTORY_PASSWORD_OPTION}) can be used.
- */
- public static final String ARTIFACTORY_API_KEY_OPTION = "artifactory-api-key";
-
- /**
- * Option that specifies if the legacy path for uploading files to artifactory should be used instead of the new
- * standard path.
- */
- public static final String ARTIFACTORY_LEGACY_PATH_OPTION = "artifactory-legacy-path";
-
- /**
- * Option that specifies under which path the coverage file shall lie within the zip file that is created for the
- * upload.
- */
- public static final String ARTIFACTORY_ZIP_PATH_OPTION = "artifactory-zip-path";
-
- /**
- * Option that specifies intermediate directories which should be appended.
- */
- public static final String ARTIFACTORY_PATH_SUFFIX = "artifactory-path-suffix";
-
- /**
- * Specifies the location of the JAR file which includes the git.properties file.
- */
- public static final String ARTIFACTORY_GIT_PROPERTIES_JAR_OPTION = "artifactory-git-properties-jar";
-
- /**
- * Specifies the date format in which the commit timestamp in the git.properties file is formatted.
- */
- public static final String ARTIFACTORY_GIT_PROPERTIES_COMMIT_DATE_FORMAT_OPTION = "artifactory-git-properties-commit-date-format";
-
- /**
- * Specifies the partition for which the upload is.
- */
- public static final String ARTIFACTORY_PARTITION = "artifactory-partition";
-
- /** Related to {@link ArtifactoryConfig#ARTIFACTORY_USER_OPTION} */
- public HttpUrl url;
-
- /** Related to {@link ArtifactoryConfig#ARTIFACTORY_USER_OPTION} */
- public String user;
-
- /** Related to {@link ArtifactoryConfig#ARTIFACTORY_PASSWORD_OPTION} */
- public String password;
-
- /** Related to {@link ArtifactoryConfig#ARTIFACTORY_LEGACY_PATH_OPTION} */
- public boolean legacyPath = false;
-
- /** Related to {@link ArtifactoryConfig#ARTIFACTORY_ZIP_PATH_OPTION} */
- public String zipPath;
-
- /** Related to {@link ArtifactoryConfig#ARTIFACTORY_PATH_SUFFIX} */
- public String pathSuffix;
-
- /** The information regarding a commit. */
- public CommitInfo commitInfo;
-
- /** Related to {@link ArtifactoryConfig#ARTIFACTORY_API_KEY_OPTION} */
- public String apiKey;
-
- /** Related to {@link ArtifactoryConfig#ARTIFACTORY_PARTITION} */
- public String partition;
-
- /**
- * Handles all command-line options prefixed with 'artifactory-'
- *
- * @return true if it has successfully processed the given option.
- */
- public static boolean handleArtifactoryOptions(ArtifactoryConfig options, String key, String value) throws AgentOptionParseException {
- switch (key) {
- case ARTIFACTORY_URL_OPTION:
- options.url = AgentOptionsParser.parseUrl(key, value);
- return true;
- case ARTIFACTORY_USER_OPTION:
- options.user = value;
- return true;
- case ARTIFACTORY_PASSWORD_OPTION:
- options.password = value;
- return true;
- case ARTIFACTORY_LEGACY_PATH_OPTION:
- options.legacyPath = Boolean.parseBoolean(value);
- return true;
- case ARTIFACTORY_ZIP_PATH_OPTION:
- options.zipPath = StringUtils.stripSuffix(value, "/");
- return true;
- case ARTIFACTORY_PATH_SUFFIX:
- options.pathSuffix = StringUtils.stripSuffix(value, "/");
- return true;
- case ARTIFACTORY_API_KEY_OPTION:
- options.apiKey = value;
- return true;
- case ARTIFACTORY_PARTITION:
- options.partition = value;
- return true;
- default:
- return false;
- }
- }
-
- /** Checks if all required options are set to upload to artifactory. */
- public boolean hasAllRequiredFieldsSet() {
- boolean requiredAuthOptionsSet = (user != null && password != null) || apiKey != null;
- boolean partitionSet = partition != null || legacyPath;
- return url != null && partitionSet && requiredAuthOptionsSet;
- }
-
- /** Checks if all required fields are null. */
- public boolean hasAllRequiredFieldsNull() {
- return url == null && user == null && password == null && apiKey == null && partition == null;
- }
-
- /** Checks whether commit and revision are set. */
- public boolean hasCommitInfo() {
- return commitInfo != null;
- }
-
- /** Parses the commit information form a git.properties file. */
- public static CommitInfo parseGitProperties(
- File jarFile, boolean searchRecursively, @Nullable DateTimeFormatter gitPropertiesCommitTimeFormat)
- throws UploaderException {
- try {
- List commitInfo = GitPropertiesLocatorUtils.getCommitInfoFromGitProperties(jarFile, true, searchRecursively, gitPropertiesCommitTimeFormat);
- if (commitInfo.isEmpty()) {
- throw new UploaderException("Found no git.properties files in " + jarFile);
- }
- if (commitInfo.size() > 1) {
- throw new UploaderException("Found multiple git.properties files in " + jarFile
- + ". Uploading to multiple projects is currently not possible with Artifactory. "
- + "Please contact CQSE if you need this feature.");
- }
- return commitInfo.get(0);
- } catch (IOException | InvalidGitPropertiesException e) {
- throw new UploaderException("Could not locate a valid git.properties file in " + jarFile, e);
- }
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/upload/artifactory/ArtifactoryUploader.java b/agent/src/main/java/com/teamscale/jacoco/agent/upload/artifactory/ArtifactoryUploader.java
deleted file mode 100644
index 248fdc700..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/upload/artifactory/ArtifactoryUploader.java
+++ /dev/null
@@ -1,157 +0,0 @@
-package com.teamscale.jacoco.agent.upload.artifactory;
-
-import com.teamscale.client.CommitDescriptor;
-import com.teamscale.client.EReportFormat;
-import com.teamscale.client.FileSystemUtils;
-import com.teamscale.client.HttpUtils;
-import com.teamscale.client.StringUtils;
-import com.teamscale.jacoco.agent.commit_resolution.git_properties.CommitInfo;
-import com.teamscale.jacoco.agent.upload.HttpZipUploaderBase;
-import com.teamscale.jacoco.agent.upload.IUploadRetry;
-import com.teamscale.report.jacoco.CoverageFile;
-import okhttp3.Interceptor;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.ResponseBody;
-import retrofit2.Response;
-
-import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.nio.file.Path;
-import java.util.List;
-import java.util.Properties;
-
-import static com.teamscale.jacoco.agent.upload.teamscale.ETeamscaleServerProperties.COMMIT;
-import static com.teamscale.jacoco.agent.upload.teamscale.ETeamscaleServerProperties.PARTITION;
-import static com.teamscale.jacoco.agent.upload.teamscale.ETeamscaleServerProperties.REVISION;
-import static com.teamscale.jacoco.agent.upload.teamscale.TeamscaleUploader.RETRY_UPLOAD_FILE_SUFFIX;
-
-/**
- * Uploads XMLs to Artifactory.
- */
-public class ArtifactoryUploader extends HttpZipUploaderBase implements IUploadRetry {
-
- /**
- * Header that can be used as alternative to basic authentication to authenticate requests against artifactory. For
- * details check https://www.jfrog.com/confluence/display/JFROG/Artifactory+REST+API
- */
- public static final String ARTIFACTORY_API_HEADER = "X-JFrog-Art-Api";
- private final ArtifactoryConfig artifactoryConfig;
- private final String coverageFormat;
- private String uploadPath;
-
- /** Constructor. */
- public ArtifactoryUploader(ArtifactoryConfig config, List additionalMetaDataFiles,
- EReportFormat reportFormat) {
- super(config.url, additionalMetaDataFiles, IArtifactoryUploadApi.class);
- this.artifactoryConfig = config;
- this.coverageFormat = reportFormat.name().toLowerCase();
- }
-
- @Override
- public void markFileForUploadRetry(CoverageFile coverageFile) {
- File uploadMetadataFile = new File(FileSystemUtils.replaceFilePathFilenameWith(
- FileSystemUtils.normalizeSeparators(coverageFile.toString()),
- coverageFile.getName() + RETRY_UPLOAD_FILE_SUFFIX));
- Properties properties = createArtifactoryProperties();
- try (FileWriter writer = new FileWriter(uploadMetadataFile)) {
- properties.store(writer, null);
- } catch (IOException e) {
- logger.warn(
- "Failed to create metadata file for automatic upload retry of {}. Please manually retry the coverage upload to Azure.",
- coverageFile);
- uploadMetadataFile.delete();
- }
- }
-
- @Override
- public void reupload(CoverageFile coverageFile, Properties reuploadProperties) {
- ArtifactoryConfig config = new ArtifactoryConfig();
- config.url = artifactoryConfig.url;
- config.user = artifactoryConfig.user;
- config.password = artifactoryConfig.password;
- config.legacyPath = artifactoryConfig.legacyPath;
- config.zipPath = artifactoryConfig.zipPath;
- config.pathSuffix = artifactoryConfig.pathSuffix;
- String revision = reuploadProperties.getProperty(REVISION.name());
- String commitString = reuploadProperties.getProperty(COMMIT.name());
- config.commitInfo = new CommitInfo(revision, CommitDescriptor.parse(commitString));
- config.apiKey = artifactoryConfig.apiKey;
- config.partition = StringUtils.emptyToNull(reuploadProperties.getProperty(PARTITION.name()));
- setUploadPath(coverageFile, config);
- super.upload(coverageFile);
- }
-
- /** Creates properties from the artifactory configs. */
- private Properties createArtifactoryProperties() {
- Properties properties = new Properties();
- properties.setProperty(REVISION.name(), artifactoryConfig.commitInfo.revision);
- properties.setProperty(COMMIT.name(), artifactoryConfig.commitInfo.commit.toString());
- properties.setProperty(PARTITION.name(), StringUtils.nullToEmpty(artifactoryConfig.partition));
- return properties;
- }
-
- @Override
- protected void configureOkHttp(OkHttpClient.Builder builder) {
- super.configureOkHttp(builder);
- if (artifactoryConfig.apiKey != null) {
- builder.addInterceptor(getArtifactoryApiHeaderInterceptor());
- } else {
- builder.addInterceptor(
- HttpUtils.getBasicAuthInterceptor(artifactoryConfig.user, artifactoryConfig.password));
- }
- }
-
- private void setUploadPath(CoverageFile coverageFile, ArtifactoryConfig artifactoryConfig) {
- if (artifactoryConfig.legacyPath) {
- this.uploadPath = String.join("/", artifactoryConfig.commitInfo.commit.branchName,
- artifactoryConfig.commitInfo.commit.timestamp + "-" + artifactoryConfig.commitInfo.revision,
- coverageFile.getNameWithoutExtension() + ".zip");
- } else if (artifactoryConfig.pathSuffix == null) {
- this.uploadPath = String.join("/", "uploads", artifactoryConfig.commitInfo.commit.branchName,
- artifactoryConfig.commitInfo.commit.timestamp + "-" + artifactoryConfig.commitInfo.revision,
- artifactoryConfig.partition, coverageFormat, coverageFile.getNameWithoutExtension() + ".zip");
- } else {
- this.uploadPath = String.join("/", "uploads", artifactoryConfig.commitInfo.commit.branchName,
- artifactoryConfig.commitInfo.commit.timestamp + "-" + artifactoryConfig.commitInfo.revision,
- artifactoryConfig.partition, coverageFormat, artifactoryConfig.pathSuffix,
- coverageFile.getNameWithoutExtension() + ".zip");
- }
- }
-
- @Override
- public void upload(CoverageFile coverageFile) {
- setUploadPath(coverageFile, this.artifactoryConfig);
- super.upload(coverageFile);
- }
-
- @Override
- protected Response uploadCoverageZip(File zipFile) throws IOException {
- return getApi().uploadCoverageZip(uploadPath, zipFile);
- }
-
- @Override
- protected String getZipEntryCoverageFileName(CoverageFile coverageFile) {
- String path = coverageFile.getName();
- if (!StringUtils.isEmpty(artifactoryConfig.zipPath)) {
- path = artifactoryConfig.zipPath + "/" + path;
- }
-
- return path;
- }
-
- /** {@inheritDoc} */
- @Override
- public String describe() {
- return "Uploading to " + uploadUrl;
- }
-
- private Interceptor getArtifactoryApiHeaderInterceptor() {
- return chain -> {
- Request newRequest = chain.request().newBuilder().header(ARTIFACTORY_API_HEADER, artifactoryConfig.apiKey)
- .build();
- return chain.proceed(newRequest);
- };
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/upload/artifactory/IArtifactoryUploadApi.java b/agent/src/main/java/com/teamscale/jacoco/agent/upload/artifactory/IArtifactoryUploadApi.java
deleted file mode 100644
index 316e48a14..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/upload/artifactory/IArtifactoryUploadApi.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*-------------------------------------------------------------------------+
-| |
-| Copyright (c) 2009-2018 CQSE GmbH |
-| |
-+-------------------------------------------------------------------------*/
-package com.teamscale.jacoco.agent.upload.artifactory;
-
-import okhttp3.MediaType;
-import okhttp3.RequestBody;
-import okhttp3.ResponseBody;
-import retrofit2.Call;
-import retrofit2.Response;
-import retrofit2.Retrofit;
-import retrofit2.http.Body;
-import retrofit2.http.PUT;
-import retrofit2.http.Path;
-
-import java.io.File;
-import java.io.IOException;
-
-/** {@link Retrofit} API specification for the {@link ArtifactoryUploader}. */
-public interface IArtifactoryUploadApi {
-
- /** The upload API call. */
- @PUT("{path}")
- Call upload(@Path("path") String path, @Body RequestBody uploadedFile);
-
- /**
- * Convenience method to perform an upload for a coverage zip.
- */
- default Response uploadCoverageZip(String path, File data) throws IOException {
- RequestBody body = RequestBody.create(MediaType.parse("application/zip"), data);
- return upload(path, body).execute();
- }
-
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/upload/delay/DelayedUploader.java b/agent/src/main/java/com/teamscale/jacoco/agent/upload/delay/DelayedUploader.java
deleted file mode 100644
index 178aee399..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/upload/delay/DelayedUploader.java
+++ /dev/null
@@ -1,116 +0,0 @@
-package com.teamscale.jacoco.agent.upload.delay;
-
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-import java.util.function.Function;
-import java.util.stream.Stream;
-
-import org.slf4j.Logger;
-
-import com.teamscale.jacoco.agent.upload.IUploader;
-import com.teamscale.jacoco.agent.util.DaemonThreadFactory;
-import com.teamscale.jacoco.agent.logging.LoggingUtils;
-import com.teamscale.report.jacoco.CoverageFile;
-
-/**
- * Wraps an {@link IUploader} and in order to delay upload until a all
- * information describing a commit is asynchronously made available.
- */
-public class DelayedUploader implements IUploader {
-
- private final Executor executor;
- private final Logger logger = LoggingUtils.getLogger(this);
- private final Function wrappedUploaderFactory;
- private IUploader wrappedUploader = null;
- private final Path cacheDir;
-
- public DelayedUploader(Function wrappedUploaderFactory, Path cacheDir) {
- this(wrappedUploaderFactory, cacheDir, Executors.newSingleThreadExecutor(
- new DaemonThreadFactory(DelayedUploader.class, "Delayed cache upload thread")));
- }
-
- /**
- * Visible for testing. Allows tests to control the {@link Executor} to test the
- * asynchronous functionality of this class.
- */
- /* package */ DelayedUploader(Function wrappedUploaderFactory, Path cacheDir, Executor executor) {
- this.wrappedUploaderFactory = wrappedUploaderFactory;
- this.cacheDir = cacheDir;
- this.executor = executor;
-
- registerShutdownHook();
- }
-
- private void registerShutdownHook() {
- Runtime.getRuntime().addShutdownHook(new Thread(() -> {
- if (wrappedUploader == null) {
- logger.error("The application was shut down before a commit could be found. The recorded coverage"
- + " is still cached in {} but will not be automatically processed. You configured the"
- + " agent to auto-detect the commit to which the recorded coverage should be uploaded to"
- + " Teamscale. In order to fix this problem, you need to provide a git.properties file"
- + " in all of the profiled Jar/War/Ear/... files. If you're using Gradle or"
- + " Maven, you can use a plugin to create a proper git.properties file for you, see"
- + " https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto-git-info"
- + "\nTo debug problems with git.properties, please enable debug logging for the agent via"
- + " the logging-config parameter.", cacheDir.toAbsolutePath());
- }
- }));
- }
-
- @Override
- public synchronized void upload(CoverageFile file) {
- if (wrappedUploader == null) {
- logger.info("The commit to upload to has not yet been found. Caching coverage XML in {}",
- cacheDir.toAbsolutePath());
- } else {
- wrappedUploader.upload(file);
- }
- }
-
- @Override
- public String describe() {
- if (wrappedUploader != null) {
- return wrappedUploader.describe();
- }
- return "Temporary cache until commit is resolved: " + cacheDir.toAbsolutePath();
- }
-
- /**
- * Sets the commit to upload the XMLs to and asynchronously triggers the upload
- * of all cached XMLs. This method should only be called once.
- */
- public synchronized void setCommitAndTriggerAsynchronousUpload(T information) {
- if (wrappedUploader == null) {
- wrappedUploader = wrappedUploaderFactory.apply(information);
- logger.info("Commit to upload to has been found: {}. Uploading any cached XMLs now to {}", information,
- wrappedUploader.describe());
- executor.execute(this::uploadCachedXmls);
- } else {
- logger.error(
- "Tried to set upload commit multiple times (old uploader: {}, new commit: {})."
- + " This is a programming error. Please report a bug.",
- wrappedUploader.describe(), information);
- }
- }
-
- private void uploadCachedXmls() {
- try {
- if (!Files.isDirectory(cacheDir)) {
- // Found data before XML was dumped
- return;
- }
- Stream xmlFilesStream = Files.list(cacheDir).filter(path -> {
- String fileName = path.getFileName().toString();
- return fileName.startsWith("jacoco-") && fileName.endsWith(".xml");
- });
- xmlFilesStream.forEach(path -> wrappedUploader.upload(new CoverageFile(path.toFile())));
- logger.debug("Finished upload of cached XMLs to {}", wrappedUploader.describe());
- } catch (IOException e) {
- logger.error("Failed to list cached coverage XML files in {}", cacheDir.toAbsolutePath(), e);
- }
-
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/upload/teamscale/DelayedTeamscaleMultiProjectUploader.java b/agent/src/main/java/com/teamscale/jacoco/agent/upload/teamscale/DelayedTeamscaleMultiProjectUploader.java
deleted file mode 100644
index 5c325925e..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/upload/teamscale/DelayedTeamscaleMultiProjectUploader.java
+++ /dev/null
@@ -1,55 +0,0 @@
-package com.teamscale.jacoco.agent.upload.teamscale;
-
-import com.teamscale.client.TeamscaleServer;
-import com.teamscale.jacoco.agent.commit_resolution.git_properties.CommitInfo;
-import com.teamscale.jacoco.agent.options.ProjectAndCommit;
-import com.teamscale.jacoco.agent.upload.DelayedMultiUploaderBase;
-import com.teamscale.jacoco.agent.upload.IUploader;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.function.BiFunction;
-
-/** Wrapper for {@link TeamscaleUploader} that allows to upload the same coverage file to multiple Teamscale projects. */
-public class DelayedTeamscaleMultiProjectUploader extends DelayedMultiUploaderBase implements IUploader {
-
- private final BiFunction teamscaleServerFactory;
- private final List teamscaleUploaders = new ArrayList<>();
-
- public DelayedTeamscaleMultiProjectUploader(
- BiFunction teamscaleServerFactory) {
- this.teamscaleServerFactory = teamscaleServerFactory;
- }
-
- public List getTeamscaleUploaders() {
- return teamscaleUploaders;
- }
-
- /**
- * Adds a teamscale project and commit as a possible new target to upload coverage to. Checks if the project and
- * commit are already registered as an upload target and will prevent duplicate uploads.
- */
- public void addTeamscaleProjectAndCommit(File file, ProjectAndCommit projectAndCommit) {
-
- TeamscaleServer teamscaleServer = teamscaleServerFactory.apply(projectAndCommit.getProject(),
- projectAndCommit.getCommitInfo());
-
- if (this.teamscaleUploaders.stream().anyMatch(teamscaleUploader ->
- teamscaleUploader.getTeamscaleServer().hasSameProjectAndCommit(teamscaleServer)
- )) {
- logger.debug(
- "Project and commit in git.properties file {} are already registered as upload target. Coverage will not be uploaded multiple times to the same project {} and commit info {}.",
- file, projectAndCommit.getProject(), projectAndCommit.getCommitInfo());
- return;
- }
- TeamscaleUploader uploader = new TeamscaleUploader(teamscaleServer);
- teamscaleUploaders.add(uploader);
- }
-
- @Override
- protected Collection getWrappedUploaders() {
- return new ArrayList<>(teamscaleUploaders);
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/upload/teamscale/ETeamscaleServerProperties.java b/agent/src/main/java/com/teamscale/jacoco/agent/upload/teamscale/ETeamscaleServerProperties.java
deleted file mode 100644
index e2bfb413f..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/upload/teamscale/ETeamscaleServerProperties.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.teamscale.jacoco.agent.upload.teamscale;
-
-import com.teamscale.client.TeamscaleServer;
-
-/** Describes all the fields of the {@link TeamscaleServer}. */
-public enum ETeamscaleServerProperties {
-
- /** See {@link TeamscaleServer#url} */
- URL,
- /** See {@link TeamscaleServer#project} */
- PROJECT,
- /** See {@link TeamscaleServer#userName} */
- USER_NAME,
- /** See {@link TeamscaleServer#userAccessToken} */
- USER_ACCESS_TOKEN,
- /** See {@link TeamscaleServer#partition} */
- PARTITION,
- /** See {@link TeamscaleServer#commit} */
- COMMIT,
- /** See {@link TeamscaleServer#revision} */
- REVISION,
- /** See {@link TeamscaleServer#repository} */
- REPOSITORY,
- /** See {@link TeamscaleServer#getMessage()} */
- MESSAGE;
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/upload/teamscale/TeamscaleUploader.java b/agent/src/main/java/com/teamscale/jacoco/agent/upload/teamscale/TeamscaleUploader.java
deleted file mode 100644
index 39517e38e..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/upload/teamscale/TeamscaleUploader.java
+++ /dev/null
@@ -1,156 +0,0 @@
-package com.teamscale.jacoco.agent.upload.teamscale;
-
-import com.teamscale.client.CommitDescriptor;
-import com.teamscale.client.EReportFormat;
-import com.teamscale.client.FileSystemUtils;
-import com.teamscale.client.ITeamscaleService;
-import com.teamscale.client.ITeamscaleServiceKt;
-import com.teamscale.client.StringUtils;
-import com.teamscale.client.TeamscaleServer;
-import com.teamscale.client.TeamscaleServiceGenerator;
-import com.teamscale.jacoco.agent.logging.LoggingUtils;
-import com.teamscale.jacoco.agent.upload.IUploadRetry;
-import com.teamscale.jacoco.agent.upload.IUploader;
-import com.teamscale.jacoco.agent.util.AgentUtils;
-import com.teamscale.jacoco.agent.util.Benchmark;
-import com.teamscale.report.jacoco.CoverageFile;
-import org.slf4j.Logger;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.OutputStreamWriter;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.util.Properties;
-
-import static com.teamscale.jacoco.agent.upload.teamscale.ETeamscaleServerProperties.COMMIT;
-import static com.teamscale.jacoco.agent.upload.teamscale.ETeamscaleServerProperties.MESSAGE;
-import static com.teamscale.jacoco.agent.upload.teamscale.ETeamscaleServerProperties.PARTITION;
-import static com.teamscale.jacoco.agent.upload.teamscale.ETeamscaleServerProperties.PROJECT;
-import static com.teamscale.jacoco.agent.upload.teamscale.ETeamscaleServerProperties.REPOSITORY;
-import static com.teamscale.jacoco.agent.upload.teamscale.ETeamscaleServerProperties.REVISION;
-
-/** Uploads XML Coverage to a Teamscale instance. */
-public class TeamscaleUploader implements IUploader, IUploadRetry {
-
- /**
- * The properties file suffix for unsuccessful coverage uploads.
- */
- public static final String RETRY_UPLOAD_FILE_SUFFIX = "_upload-retry.properties";
-
- /** The logger. */
- private final Logger logger = LoggingUtils.getLogger(this);
-
- public TeamscaleServer getTeamscaleServer() {
- return teamscaleServer;
- }
-
- /** Teamscale server details. */
- private final TeamscaleServer teamscaleServer;
-
- /** Constructor. */
- public TeamscaleUploader(TeamscaleServer teamscaleServer) {
- this.teamscaleServer = teamscaleServer;
- }
-
- @Override
- public void upload(CoverageFile coverageFile) {
- doUpload(coverageFile, this.teamscaleServer);
- }
-
- @Override
- public void reupload(CoverageFile coverageFile, Properties reuploadProperties) {
- TeamscaleServer server = new TeamscaleServer();
- server.project = reuploadProperties.getProperty(PROJECT.name());
- server.commit = CommitDescriptor.parse(reuploadProperties.getProperty(COMMIT.name()));
- server.partition = reuploadProperties.getProperty(PARTITION.name());
- server.revision = StringUtils.emptyToNull(reuploadProperties.getProperty(REVISION.name()));
- server.repository = StringUtils.emptyToNull(reuploadProperties.getProperty(REPOSITORY.name()));
- server.userAccessToken = teamscaleServer.userAccessToken;
- server.userName = teamscaleServer.userName;
- server.url = teamscaleServer.url;
- server.setMessage(reuploadProperties.getProperty(MESSAGE.name()));
- doUpload(coverageFile, server);
- }
-
- private void doUpload(CoverageFile coverageFile, TeamscaleServer teamscaleServer) {
- try (Benchmark benchmark = new Benchmark("Uploading report to Teamscale")) {
- if (tryUploading(coverageFile, teamscaleServer)) {
- deleteCoverageFile(coverageFile);
- } else {
- logger.warn("Failed to upload coverage to Teamscale. "
- + "Won't delete local file {} so that the upload can automatically be retried upon profiler restart. "
- + "Upload can also be retried manually.", coverageFile);
- markFileForUploadRetry(coverageFile);
- }
- }
- }
-
- @Override
- public void markFileForUploadRetry(CoverageFile coverageFile) {
- File uploadMetadataFile = new File(FileSystemUtils.replaceFilePathFilenameWith(
- FileSystemUtils.normalizeSeparators(coverageFile.toString()),
- coverageFile.getName() + RETRY_UPLOAD_FILE_SUFFIX));
- Properties serverProperties = this.createServerProperties();
- try (OutputStreamWriter writer = new OutputStreamWriter(Files.newOutputStream(uploadMetadataFile.toPath()),
- StandardCharsets.UTF_8)) {
- serverProperties.store(writer, null);
- } catch (IOException e) {
- logger.warn(
- "Failed to create metadata file for automatic upload retry of {}. Please manually retry the coverage upload to Teamscale.",
- coverageFile);
- uploadMetadataFile.delete();
- }
- }
-
- /**
- * Creates server properties to be written in a properties file.
- */
- private Properties createServerProperties() {
- Properties serverProperties = new Properties();
- serverProperties.setProperty(PROJECT.name(), teamscaleServer.project);
- serverProperties.setProperty(PARTITION.name(), teamscaleServer.partition);
- if (teamscaleServer.commit != null) {
- serverProperties.setProperty(COMMIT.name(), teamscaleServer.commit.toString());
- }
- serverProperties.setProperty(REVISION.name(), StringUtils.nullToEmpty(teamscaleServer.revision));
- serverProperties.setProperty(REPOSITORY.name(), StringUtils.nullToEmpty(teamscaleServer.repository));
- serverProperties.setProperty(MESSAGE.name(), teamscaleServer.getMessage());
- return serverProperties;
- }
-
- private void deleteCoverageFile(CoverageFile coverageFile) {
- try {
- coverageFile.delete();
- } catch (IOException e) {
- logger.warn("The upload to Teamscale was successful, but the deletion of the coverage file {} failed. "
- + "You can delete it yourself anytime - it is no longer needed.", coverageFile, e);
- }
- }
-
- /** Performs the upload and returns true if successful. */
- private boolean tryUploading(CoverageFile coverageFile, TeamscaleServer teamscaleServer) {
- logger.debug("Uploading JaCoCo artifact to {}", teamscaleServer);
-
- try {
- // Cannot be executed in the constructor as this causes issues in WildFly server
- // (See #100)
- ITeamscaleService api = TeamscaleServiceGenerator.createService(ITeamscaleService.class,
- teamscaleServer.url, teamscaleServer.userName, teamscaleServer.userAccessToken,
- AgentUtils.USER_AGENT);
- ITeamscaleServiceKt.uploadReport(api, teamscaleServer.project, teamscaleServer.commit,
- teamscaleServer.revision,
- teamscaleServer.repository, teamscaleServer.partition, EReportFormat.JACOCO,
- teamscaleServer.getMessage(), coverageFile.createFormRequestBody());
- return true;
- } catch (IOException e) {
- logger.error("Failed to upload coverage to {}", teamscaleServer, e);
- return false;
- }
- }
-
- @Override
- public String describe() {
- return "Uploading to " + teamscaleServer;
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/util/AgentUtils.java b/agent/src/main/java/com/teamscale/jacoco/agent/util/AgentUtils.java
deleted file mode 100644
index e055ee8c1..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/util/AgentUtils.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package com.teamscale.jacoco.agent.util;
-
-import com.teamscale.client.FileSystemUtils;
-import com.teamscale.client.TeamscaleServiceGenerator;
-import com.teamscale.jacoco.agent.PreMain;
-import com.teamscale.jacoco.agent.configuration.ProcessInformationRetriever;
-
-import java.io.IOException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ResourceBundle;
-
-/** General utilities for working with the agent. */
-public class AgentUtils {
-
- /** Version of this program. */
- public static final String VERSION;
-
- /** User-Agent header value for HTTP requests. */
- public static final String USER_AGENT;
-
- private static Path mainTempDirectory = null;
-
- static {
- ResourceBundle bundle = ResourceBundle.getBundle("com.teamscale.jacoco.agent.app");
- VERSION = bundle.getString("version");
- USER_AGENT = TeamscaleServiceGenerator.buildUserAgent("Teamscale Java Profiler", VERSION);
- }
-
- /**
- * Returns the main temporary directory where all agent temp files should be placed.
- */
- public static Path getMainTempDirectory() {
- if (mainTempDirectory == null) {
- try {
- // We add a trailing hyphen here to visually separate the PID from the random number that Java appends
- // to the name to make it unique
- mainTempDirectory = Files.createTempDirectory("teamscale-java-profiler-" +
- FileSystemUtils.toSafeFilename(ProcessInformationRetriever.getPID()) + "-");
- } catch (IOException e) {
- throw new RuntimeException("Failed to create temporary directory for agent files", e);
- }
- }
- return mainTempDirectory;
- }
-
- /** Returns the directory that contains the agent installation. */
- public static Path getAgentDirectory() {
- try {
- URI jarFileUri = PreMain.class.getProtectionDomain().getCodeSource().getLocation().toURI();
- // we assume that the dist zip is extracted and the agent jar not moved
- Path jarDirectory = Paths.get(jarFileUri).getParent();
- Path installDirectory = jarDirectory.getParent();
- if (installDirectory == null) {
- // happens when the jar file is stored in the root directory
- return jarDirectory;
- }
- return installDirectory;
- } catch (URISyntaxException e) {
- throw new RuntimeException("Failed to obtain agent directory. This is a bug, please report it.", e);
- }
- }
-
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/util/Assertions.java b/agent/src/main/java/com/teamscale/jacoco/agent/util/Assertions.java
deleted file mode 100644
index b789c5f6a..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/util/Assertions.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package com.teamscale.jacoco.agent.util;
-
-import org.jetbrains.annotations.Contract;
-
-/**
- * Simple methods to implement assertions.
- */
-public class Assertions {
-
- /**
- * Checks if a condition is true.
- *
- * @param condition condition to check
- * @param message exception message
- * @throws AssertionError if the condition is false
- */
- @Contract(value = "false, _ -> fail", pure = true)
- public static void isTrue(boolean condition, String message) throws AssertionError {
- throwAssertionErrorIfTestFails(condition, message);
- }
-
- /**
- * Checks if a condition is false.
- *
- * @param condition condition to check
- * @param message exception message
- * @throws AssertionError if the condition is true
- */
- @Contract(value = "true, _ -> fail", pure = true)
- public static void isFalse(boolean condition, String message) throws AssertionError {
- throwAssertionErrorIfTestFails(!condition, message);
- }
-
- /**
- * Throws an {@link AssertionError} if the test fails.
- *
- * @param test test which should be true
- * @param message exception message
- * @throws AssertionError if the test fails
- */
- private static void throwAssertionErrorIfTestFails(boolean test, String message) {
- if (!test) {
- throw new AssertionError(message);
- }
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/util/DaemonThreadFactory.java b/agent/src/main/java/com/teamscale/jacoco/agent/util/DaemonThreadFactory.java
deleted file mode 100644
index 1ec3461ec..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/util/DaemonThreadFactory.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.teamscale.jacoco.agent.util;
-
-import java.util.concurrent.ThreadFactory;
-
-/**
- * {@link ThreadFactory} that only produces deamon threads (threads that don't prevent JVM shutdown) with a fixed name.
- */
-public class DaemonThreadFactory implements ThreadFactory {
-
- private final String threadName;
-
- public DaemonThreadFactory(Class> owningClass, String threadName) {
- this.threadName = "Teamscale Java Profiler " + owningClass.getSimpleName() + " " + threadName;
- }
-
- @Override
- public Thread newThread(Runnable runnable) {
- Thread thread = new Thread(runnable, threadName);
- thread.setDaemon(true);
- return thread;
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/util/NullOutputStream.java b/agent/src/main/java/com/teamscale/jacoco/agent/util/NullOutputStream.java
deleted file mode 100644
index 856e316a1..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/util/NullOutputStream.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package com.teamscale.jacoco.agent.util;
-
-import org.jetbrains.annotations.NotNull;
-
-import java.io.IOException;
-import java.io.OutputStream;
-
-/** NOP output stream implementation. */
-public class NullOutputStream extends OutputStream {
-
- public NullOutputStream() {
- // do nothing
- }
-
- @Override
- public void write(final byte @NotNull [] b, final int off, final int len) {
- // to /dev/null
- }
-
- @Override
- public void write(final int b) {
- // to /dev/null
- }
-
- @Override
- public void write(final byte @NotNull [] b) throws IOException {
- // to /dev/null
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/util/Timer.java b/agent/src/main/java/com/teamscale/jacoco/agent/util/Timer.java
deleted file mode 100644
index a6787e085..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/util/Timer.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*-------------------------------------------------------------------------+
-| |
-| Copyright (c) 2009-2018 CQSE GmbH |
-| |
-+-------------------------------------------------------------------------*/
-package com.teamscale.jacoco.agent.util;
-
-import java.time.Duration;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Triggers a callback in a regular interval. Note that the spawned threads are
- * Daemon threads, i.e. they will not prevent the JVM from shutting down.
- *
- * The timer will abort if the given {@link #runnable} ever throws an exception.
- */
-public class Timer {
-
- /** Runs the job on a background daemon thread. */
- private final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1, runnable -> {
- Thread thread = Executors.defaultThreadFactory().newThread(runnable);
- thread.setDaemon(true);
- return thread;
- });
-
- /** The currently running job or null. */
- private ScheduledFuture> job = null;
-
- /** The job to execute periodically. */
- private final Runnable runnable;
-
- /** Duration between two job executions. */
- private final Duration duration;
-
- /** Constructor. */
- public Timer(Runnable runnable, Duration duration) {
- this.runnable = runnable;
- this.duration = duration;
- }
-
- /** Starts the regular job. */
- public synchronized void start() {
- if (job != null) {
- return;
- }
-
- job = executor.scheduleAtFixedRate(runnable, duration.toMinutes(), duration.toMinutes(), TimeUnit.MINUTES);
- }
-
- /** Stops the regular job, possibly aborting it. */
- public synchronized void stop() {
- job.cancel(false);
- job = null;
- }
-
-}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/Agent.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/Agent.kt
new file mode 100644
index 000000000..010e88409
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/Agent.kt
@@ -0,0 +1,170 @@
+package com.teamscale.jacoco.agent
+
+import com.teamscale.client.FileSystemUtils
+import com.teamscale.client.StringUtils
+import com.teamscale.jacoco.agent.logging.LoggingUtils
+import com.teamscale.jacoco.agent.options.AgentOptions
+import com.teamscale.jacoco.agent.upload.IUploadRetry
+import com.teamscale.jacoco.agent.upload.IUploader
+import com.teamscale.jacoco.agent.upload.teamscale.TeamscaleUploader
+import com.teamscale.jacoco.agent.util.AgentUtils
+import com.teamscale.report.jacoco.CoverageFile
+import com.teamscale.report.jacoco.EmptyReportException
+import com.teamscale.report.jacoco.JaCoCoXmlReportGenerator
+import com.teamscale.report.jacoco.dump.Dump
+import org.glassfish.jersey.server.ResourceConfig
+import org.glassfish.jersey.server.ServerProperties
+import java.io.File
+import java.io.IOException
+import java.lang.instrument.Instrumentation
+import java.nio.file.Files
+import java.util.Timer
+import kotlin.concurrent.fixedRateTimer
+import kotlin.io.path.deleteIfExists
+import kotlin.io.path.listDirectoryEntries
+import kotlin.time.DurationUnit
+import kotlin.time.toDuration
+
+/**
+ * A wrapper around the JaCoCo Java agent that automatically triggers a dump and XML conversion based on a time
+ * interval.
+ */
+class Agent(options: AgentOptions, instrumentation: Instrumentation?) : AgentBase(options) {
+ /** Converts binary data to XML. */
+ private val generator: JaCoCoXmlReportGenerator
+
+ /** Regular dump task. */
+ private var timer: Timer? = null
+
+ /** Stores the XML files. */
+ private val uploader = options.createUploader(instrumentation)
+
+ /** Constructor. */
+ init {
+ logger.info("Upload method: {}", uploader.describe())
+ retryUnsuccessfulUploads(options, uploader)
+ generator = JaCoCoXmlReportGenerator(
+ options.getClassDirectoriesOrZips(),
+ options.locationIncludeFilter,
+ options.getDuplicateClassFileBehavior(),
+ options.shouldIgnoreUncoveredClasses(),
+ LoggingUtils.wrap(logger)
+ )
+
+ if (options.shouldDumpInIntervals()) {
+ val period = options.dumpIntervalInMinutes.toDuration(DurationUnit.MINUTES).inWholeMilliseconds
+ timer = fixedRateTimer("Teamscale-Java-Profiler", true, period, period) {
+ dumpReport()
+ }
+ logger.info("Dumping every ${options.dumpIntervalInMinutes} minutes.")
+ }
+ options.teamscaleServerOptions.partition?.let { partition ->
+ controller.sessionId = partition
+ }
+ }
+
+ /**
+ * If we have coverage that was leftover because of previously unsuccessful coverage uploads, we retry to upload
+ * them again with the same configuration as in the previous try.
+ */
+ private fun retryUnsuccessfulUploads(options: AgentOptions, uploader: IUploader) {
+ var outputPath = options.outputDirectory
+ if (outputPath == null) {
+ // Default fallback
+ outputPath = AgentUtils.agentDirectory.resolve("coverage")
+ }
+
+ val parentPath = outputPath.parent
+ if (parentPath == null) {
+ logger.error("The output path '{}' does not have a parent path. Canceling upload retry.", outputPath.toAbsolutePath())
+ return
+ }
+
+ parentPath.toFile().walk()
+ .filter { it.name.endsWith(TeamscaleUploader.RETRY_UPLOAD_FILE_SUFFIX) }
+ .forEach { file ->
+ reuploadCoverageFromPropertiesFile(file, uploader)
+ }
+ }
+
+ private fun reuploadCoverageFromPropertiesFile(file: File, uploader: IUploader) {
+ logger.info("Retrying previously unsuccessful coverage upload for file {}.", file)
+ try {
+ val properties = FileSystemUtils.readProperties(file)
+ val coverageFile = CoverageFile(
+ File(StringUtils.stripSuffix(file.absolutePath, TeamscaleUploader.RETRY_UPLOAD_FILE_SUFFIX))
+ )
+
+ if (uploader is IUploadRetry) {
+ uploader.reupload(coverageFile, properties)
+ } else {
+ logger.info("Reupload not implemented for uploader {}", uploader.describe())
+ }
+ Files.deleteIfExists(file.toPath())
+ } catch (e: IOException) {
+ logger.error("Reuploading coverage failed. $e")
+ }
+ }
+
+ override fun initResourceConfig(): ResourceConfig? {
+ val resourceConfig = ResourceConfig()
+ resourceConfig.property(ServerProperties.WADL_FEATURE_DISABLE, true.toString())
+ AgentResource.setAgent(this)
+ return resourceConfig.register(AgentResource::class.java).register(GenericExceptionMapper::class.java)
+ }
+
+ override fun prepareShutdown() {
+ timer?.cancel()
+ if (options.shouldDumpOnExit()) dumpReport()
+
+ val dir = options.outputDirectory
+ try {
+ if (dir.listDirectoryEntries().isEmpty()) dir.deleteIfExists()
+ } catch (e: IOException) {
+ logger.info(
+ ("Could not delete empty output directory {}. "
+ + "This directory was created inside the configured output directory to be able to "
+ + "distinguish between different runs of the profiled JVM. You may delete it manually."),
+ dir, e
+ )
+ }
+ }
+
+ /**
+ * Dumps the current execution data, converts it, writes it to the output directory defined in [.options] and
+ * uploads it if an uploader is configured. Logs any errors, never throws an exception.
+ */
+ override fun dumpReport() {
+ logger.debug("Starting dump")
+
+ try {
+ dumpReportUnsafe()
+ } catch (t: Throwable) {
+ // we want to catch anything in order to avoid crashing the whole system under
+ // test
+ logger.error("Dump job failed with an exception", t)
+ }
+ }
+
+ private fun dumpReportUnsafe() {
+ val dump: Dump
+ try {
+ dump = controller.dumpAndReset()
+ } catch (e: JacocoRuntimeController.DumpException) {
+ logger.error("Dumping failed, retrying later", e)
+ return
+ }
+
+ try {
+ benchmark("Generating the XML report") {
+ val outputFile = options.createNewFileInOutputDirectory("jacoco", "xml")
+ val coverageFile = generator.convertSingleDumpToReport(dump, outputFile)
+ uploader.upload(coverageFile)
+ }
+ } catch (e: IOException) {
+ logger.error("Converting binary dump to XML failed", e)
+ } catch (e: EmptyReportException) {
+ logger.error("No coverage was collected. ${e.message}", e)
+ }
+ }
+}
\ 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
new file mode 100644
index 000000000..bfed53514
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/AgentBase.kt
@@ -0,0 +1,150 @@
+package com.teamscale.jacoco.agent
+
+import com.teamscale.jacoco.agent.logging.LoggingUtils
+import com.teamscale.jacoco.agent.options.AgentOptions
+import org.eclipse.jetty.server.Server
+import org.eclipse.jetty.server.ServerConnector
+import org.eclipse.jetty.servlet.ServletContextHandler
+import org.eclipse.jetty.servlet.ServletHolder
+import org.eclipse.jetty.util.thread.QueuedThreadPool
+import org.glassfish.jersey.server.ResourceConfig
+import org.glassfish.jersey.servlet.ServletContainer
+import org.jacoco.agent.rt.RT
+import org.slf4j.Logger
+import java.lang.management.ManagementFactory
+
+/**
+ * Base class for agent implementations. Handles logger shutdown, store creation and instantiation of the
+ * [JacocoRuntimeController].
+ *
+ *
+ * Subclasses must handle dumping onto disk and uploading via the configured uploader.
+ */
+abstract class AgentBase(
+ /** The agent options. */
+ @JvmField var options: AgentOptions
+) {
+ /** The logger. */
+ val logger: Logger = LoggingUtils.getLogger(this)
+
+ /** Controls the JaCoCo runtime. */
+ @JvmField
+ val controller: JacocoRuntimeController
+
+ private lateinit var server: Server
+
+ /**
+ * Lazily generated string representation of the command line arguments to print to the log.
+ */
+ private val optionsObjectToLog by lazy {
+ object {
+ override fun toString() =
+ if (options.shouldObfuscateSecurityRelatedOutputs()) {
+ options.getObfuscatedOptionsString()
+ } else {
+ options.getOriginalOptionsString()
+ }
+ }
+ }
+
+ init {
+ try {
+ controller = JacocoRuntimeController(RT.getAgent())
+ } catch (e: IllegalStateException) {
+ throw IllegalStateException("Teamscale Java Profiler not started or there is a conflict with another agent on the classpath.", e)
+ }
+ logger.info(
+ "Starting Teamscale Java Profiler for process {} with options: {}",
+ ManagementFactory.getRuntimeMXBean().name, optionsObjectToLog
+ )
+ options.getHttpServerPort()?.let { port ->
+ 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)
+ }
+ }
+ }
+
+ /**
+ * Starts the http server, which waits for information about started and finished tests.
+ */
+ @Throws(Exception::class)
+ private fun initServer() {
+ logger.info("Listening for test events on port {}.", options.getHttpServerPort())
+
+ // Jersey Implementation
+ val handler = buildUsingResourceConfig()
+ val threadPool = QueuedThreadPool()
+ threadPool.maxThreads = 10
+ threadPool.isDaemon = true
+
+ // Create a server instance and set the thread pool
+ server = Server(threadPool)
+ // Create a server connector, set the port and add it to the server
+ val connector = ServerConnector(server)
+ connector.port = options.getHttpServerPort()
+ server.addConnector(connector)
+ server.handler = handler
+ server.start()
+ }
+
+ private fun buildUsingResourceConfig(): ServletContextHandler {
+ val handler = ServletContextHandler(ServletContextHandler.NO_SESSIONS)
+ handler.contextPath = "/"
+
+ val resourceConfig = initResourceConfig()
+ handler.addServlet(ServletHolder(ServletContainer(resourceConfig)), "/*")
+ return handler
+ }
+
+ /**
+ * Initializes the [ResourceConfig] needed for the Jetty + Jersey Server
+ */
+ protected abstract fun initResourceConfig(): ResourceConfig?
+
+ /**
+ * Registers a shutdown hook that stops the timer and dumps coverage a final time.
+ */
+ fun registerShutdownHook() {
+ Runtime.getRuntime().addShutdownHook(Thread {
+ try {
+ logger.info("Teamscale Java Profiler is shutting down...")
+ stopServer()
+ prepareShutdown()
+ logger.info("Teamscale Java Profiler successfully shut down.")
+ } catch (e: Exception) {
+ logger.error("Exception during profiler shutdown.", e)
+ } finally {
+ // Try to flush logging resources also in case of an exception during shutdown
+ PreMain.closeLoggingResources()
+ }
+ })
+ }
+
+ /** Stop the http server if it's running */
+ fun stopServer() {
+ options.getHttpServerPort()?.let {
+ try {
+ server.stop()
+ } catch (e: Exception) {
+ logger.error("Could not stop server so it is killed now.", e)
+ } finally {
+ server.destroy()
+ }
+ }
+ }
+
+ /** Called when the shutdown hook is triggered. */
+ protected open fun prepareShutdown() {
+ // Template method to be overridden by subclasses.
+ }
+
+ /**
+ * Dumps the current execution data, converts it, writes it to the output
+ * directory defined in [.options] and uploads it if an uploader is
+ * configured. Logs any errors, never throws an exception.
+ */
+ abstract fun dumpReport()
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/AgentResource.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/AgentResource.kt
new file mode 100644
index 000000000..94447f400
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/AgentResource.kt
@@ -0,0 +1,41 @@
+package com.teamscale.jacoco.agent
+
+import javax.ws.rs.POST
+import javax.ws.rs.Path
+import javax.ws.rs.core.Response
+
+/**
+ * The resource of the Jersey + Jetty http server holding all the endpoints specific for the [Agent].
+ */
+@Path("/")
+class AgentResource : ResourceBase() {
+ /** Handles dumping a XML coverage report for coverage collected until now. */
+ @POST
+ @Path("/dump")
+ fun handleDump(): Response? {
+ logger.debug("Dumping report triggered via HTTP request")
+ agent.dumpReport()
+ return Response.noContent().build()
+ }
+
+ /** Handles resetting of coverage. */
+ @POST
+ @Path("/reset")
+ fun handleReset(): Response? {
+ logger.debug("Resetting coverage triggered via HTTP request")
+ agent.controller.reset()
+ return Response.noContent().build()
+ }
+
+ companion object {
+ private lateinit var agent: Agent
+
+ /**
+ * Static setter to inject the [Agent] to the resource.
+ */
+ fun setAgent(agent: Agent) {
+ Companion.agent = agent
+ agentBase = agent
+ }
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/DelayedLogger.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/DelayedLogger.kt
new file mode 100644
index 000000000..d242ff79d
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/DelayedLogger.kt
@@ -0,0 +1,52 @@
+package com.teamscale.jacoco.agent
+
+import com.teamscale.report.util.ILogger
+import org.slf4j.Logger
+import java.util.function.Consumer
+
+/**
+ * A logger that buffers logs in memory and writes them to the actual logger at a later point. This is needed when stuff
+ * needs to be logged before the actual logging framework is initialized.
+ */
+class DelayedLogger : ILogger {
+ /** List of log actions that will be executed once the logger is initialized. */
+ private val logActions = mutableListOf Unit>()
+
+ override fun debug(message: String) {
+ logActions.add { debug(message) }
+ }
+
+ override fun info(message: String) {
+ logActions.add { info(message) }
+ }
+
+ override fun warn(message: String) {
+ logActions.add { warn(message) }
+ }
+
+ override fun warn(message: String, throwable: Throwable?) {
+ logActions.add { warn(message, throwable) }
+ }
+
+ override fun error(throwable: Throwable) {
+ logActions.add { error(throwable.message, throwable) }
+ }
+
+ override fun error(message: String, throwable: Throwable?) {
+ logActions.add { error(message, throwable) }
+ }
+
+ /**
+ * Logs an error and also writes the message to [System.err] to ensure the message is even logged in case
+ * setting up the logger itself fails for some reason (see TS-23151).
+ */
+ fun errorAndStdErr(message: String?, throwable: Throwable?) {
+ System.err.println(message)
+ logActions.add { error(message, throwable) }
+ }
+
+ /** Writes the logs to the given slf4j logger. */
+ fun logTo(logger: Logger) {
+ logActions.forEach { action -> action(logger) }
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/GenericExceptionMapper.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/GenericExceptionMapper.kt
new file mode 100644
index 000000000..c7f0f9909
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/GenericExceptionMapper.kt
@@ -0,0 +1,18 @@
+package com.teamscale.jacoco.agent
+
+import javax.ws.rs.core.MediaType
+import javax.ws.rs.core.Response
+import javax.ws.rs.ext.ExceptionMapper
+import javax.ws.rs.ext.Provider
+
+/**
+ * Generates a [javax.ws.rs.core.Response] for an exception.
+ */
+@Provider
+class GenericExceptionMapper : ExceptionMapper {
+ override fun toResponse(e: Throwable?): Response =
+ Response.status(Response.Status.INTERNAL_SERVER_ERROR).apply {
+ type(MediaType.TEXT_PLAIN_TYPE)
+ entity("Message: ${e?.message}")
+ }.build()
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/Helpers.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/Helpers.kt
new file mode 100644
index 000000000..d3dc43a97
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/Helpers.kt
@@ -0,0 +1,6 @@
+package com.teamscale.jacoco.agent
+
+import kotlin.time.measureTime
+
+fun benchmark(name: String, action: () -> Unit) =
+ measureTime { action() }.also { duration -> Main.logger.debug("$name took $duration") }
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/JacocoRuntimeController.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/JacocoRuntimeController.kt
new file mode 100644
index 000000000..0a76c0d3e
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/JacocoRuntimeController.kt
@@ -0,0 +1,118 @@
+package com.teamscale.jacoco.agent
+
+import com.teamscale.report.jacoco.dump.Dump
+import org.jacoco.agent.rt.IAgent
+import org.jacoco.core.data.ExecutionData
+import org.jacoco.core.data.ExecutionDataReader
+import org.jacoco.core.data.ExecutionDataStore
+import org.jacoco.core.data.IExecutionDataVisitor
+import org.jacoco.core.data.ISessionInfoVisitor
+import org.jacoco.core.data.SessionInfo
+import java.io.ByteArrayInputStream
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+
+/**
+ * Wrapper around JaCoCo's [RT] runtime interface.
+ *
+ *
+ * Can be used if the calling code is run in the same JVM as the agent is attached to.
+ */
+class JacocoRuntimeController
+/** Constructor. */(
+ /** JaCoCo's [RT] agent instance */
+ private val agent: IAgent
+) {
+ /** Indicates a failed dump. */
+ class DumpException(message: String?, cause: Throwable?) : Exception(message, cause)
+
+ /**
+ * Dumps execution data and resets it.
+ *
+ * @throws DumpException if dumping fails. This should never happen in real life. Dumping should simply be retried
+ * later if this ever happens.
+ */
+ @Throws(DumpException::class)
+ fun dumpAndReset(): Dump {
+ val binaryData = agent.getExecutionData(true)
+
+ try {
+ ByteArrayInputStream(binaryData).use { inputStream ->
+ ExecutionDataReader(inputStream).apply {
+ val store = ExecutionDataStore()
+ setExecutionDataVisitor { store.put(it) }
+ val sessionInfoVisitor = SessionInfoVisitor()
+ setSessionInfoVisitor(sessionInfoVisitor)
+ read()
+ return Dump(sessionInfoVisitor.sessionInfo, store)
+ }
+ }
+ } catch (e: IOException) {
+ throw DumpException("should never happen for the ByteArrayInputStream", e)
+ }
+ }
+
+ /**
+ * Dumps execution data to the given file and resets it afterwards.
+ */
+ @Throws(IOException::class)
+ fun dumpToFileAndReset(file: File) {
+ val binaryData = agent.getExecutionData(true)
+
+ FileOutputStream(file, true).use { outputStream ->
+ outputStream.write(binaryData)
+ }
+ }
+
+
+ /**
+ * Dumps execution data to a file and resets it.
+ *
+ * @throws DumpException if dumping fails. This should never happen in real life. Dumping should simply be retried
+ * later if this ever happens.
+ */
+ @Throws(DumpException::class)
+ fun dump() {
+ try {
+ agent.dump(true)
+ } catch (e: IOException) {
+ throw DumpException(e.message, e)
+ }
+ }
+
+ /** Resets already collected coverage. */
+ fun reset() {
+ agent.reset()
+ }
+
+ var sessionId: String?
+ /** Returns the current sessionId. */
+ get() = agent.sessionId
+ /**
+ * Sets the current sessionId of the agent that can be used to identify which coverage is recorded from now on.
+ */
+ set(sessionId) {
+ agent.setSessionId(sessionId)
+ }
+
+ /** Unsets the session ID so that coverage collected from now on is not attributed to the previous test. */
+ fun resetSessionId() {
+ agent.sessionId = ""
+ }
+
+ /**
+ * Receives and stores a [org.jacoco.core.data.SessionInfo]. Has a fallback dummy session in case nothing is received.
+ */
+ private class SessionInfoVisitor : ISessionInfoVisitor {
+ /** The received session info or a dummy. */
+ var sessionInfo: SessionInfo = SessionInfo(
+ "dummysession", System.currentTimeMillis(), System.currentTimeMillis()
+ )
+
+ /** {@inheritDoc} */
+ override fun visitSessionInfo(info: SessionInfo) {
+ this.sessionInfo = info
+ }
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/LenientCoverageTransformer.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/LenientCoverageTransformer.kt
new file mode 100644
index 000000000..dd9094e71
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/LenientCoverageTransformer.kt
@@ -0,0 +1,51 @@
+package com.teamscale.jacoco.agent
+
+import org.jacoco.agent.rt.internal_29a6edd.CoverageTransformer
+import org.jacoco.agent.rt.internal_29a6edd.IExceptionLogger
+import org.jacoco.agent.rt.internal_29a6edd.core.runtime.AgentOptions
+import org.jacoco.agent.rt.internal_29a6edd.core.runtime.IRuntime
+import org.slf4j.Logger
+import java.lang.instrument.IllegalClassFormatException
+import java.security.ProtectionDomain
+
+/**
+ * A class file transformer which delegates to the JaCoCo [org.jacoco.agent.rt.internal_29a6edd.CoverageTransformer] to do the actual instrumentation,
+ * but treats instrumentation errors e.g. due to unsupported class file versions more lenient by only logging them, but
+ * not bailing out completely. Those unsupported classes will not be instrumented and will therefore not be contained in
+ * the collected coverage report.
+ */
+class LenientCoverageTransformer(
+ runtime: IRuntime?,
+ options: AgentOptions,
+ private val logger: Logger
+) : CoverageTransformer(
+ runtime,
+ options,
+ // The coverage transformer only uses the logger to print an error when the instrumentation fails.
+ // We want to show our more specific error message instead, so we only log this for debugging at trace.
+ IExceptionLogger { logger.trace(it.message, it) }
+) {
+ override fun transform(
+ loader: ClassLoader?,
+ classname: String,
+ classBeingRedefined: Class<*>?,
+ protectionDomain: ProtectionDomain?,
+ classfileBuffer: ByteArray
+ ): ByteArray? {
+ try {
+ return super.transform(loader, classname, classBeingRedefined, protectionDomain, classfileBuffer)
+ } catch (e: IllegalClassFormatException) {
+ logger.error(
+ "Failed to instrument $classname. File will be skipped from instrumentation. " +
+ "No coverage will be collected for it. Exclude the file from the instrumentation or try " +
+ "updating the Teamscale Java Profiler if the file should actually be instrumented. (Cause: ${getRootCauseMessage(e)})"
+ )
+ return null
+ }
+ }
+
+ companion object {
+ private fun getRootCauseMessage(e: Throwable): String? =
+ e.cause?.let { getRootCauseMessage(it) } ?: e.message
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/Main.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/Main.kt
new file mode 100644
index 000000000..e1efc8a12
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/Main.kt
@@ -0,0 +1,81 @@
+package com.teamscale.jacoco.agent
+
+import com.beust.jcommander.JCommander
+import com.beust.jcommander.Parameter
+import com.beust.jcommander.ParameterException
+import com.teamscale.client.StringUtils
+import com.teamscale.jacoco.agent.convert.ConvertCommand
+import com.teamscale.jacoco.agent.logging.LoggingUtils
+import com.teamscale.jacoco.agent.util.AgentUtils
+import org.jacoco.core.JaCoCo
+import org.slf4j.Logger
+import kotlin.system.exitProcess
+
+/** Provides a command line interface for interacting with JaCoCo. */
+object Main {
+ /** The logger. */
+ val logger: Logger = LoggingUtils.getLogger(this)
+
+ /** The default arguments that will always be parsed. */
+ private val defaultArguments = DefaultArguments()
+
+ /** The arguments for the one-time conversion process. */
+ private val command = ConvertCommand()
+
+ /** Entry point. */
+ @Throws(Exception::class)
+ @JvmStatic
+ fun main(args: Array) {
+ parseCommandLineAndRun(args)
+ }
+
+ /**
+ * Parses the given command line arguments. Exits the program or throws an exception if the arguments are not valid.
+ * Then runs the specified command.
+ */
+ @Throws(Exception::class)
+ private fun parseCommandLineAndRun(args: Array) {
+ val builder = createJCommanderBuilder()
+ val jCommander = builder.build()
+
+ try {
+ jCommander.parse(*args)
+ } catch (e: ParameterException) {
+ handleInvalidCommandLine(jCommander, e.message)
+ }
+
+ if (defaultArguments.help) {
+ println("Teamscale Java Profiler ${AgentUtils.VERSION} compiled against JaCoCo ${JaCoCo.VERSION}")
+ jCommander.usage()
+ return
+ }
+
+ val validator = command.validate()
+ if (!validator.isValid) {
+ handleInvalidCommandLine(jCommander, StringUtils.LINE_FEED + validator.errorMessage)
+ }
+
+ logger.info("Starting Teamscale Java Profiler ${AgentUtils.VERSION} compiled against JaCoCo ${JaCoCo.VERSION}")
+ command.run()
+ }
+
+ /** Shows an informative error and help message. Then exits the program. */
+ private fun handleInvalidCommandLine(jCommander: JCommander, message: String?) {
+ System.err.println("Invalid command line: $message${StringUtils.LINE_FEED}")
+ jCommander.usage()
+ exitProcess(1)
+ }
+
+ /** Creates a builder for a [com.beust.jcommander.JCommander] object. */
+ private fun createJCommanderBuilder() =
+ JCommander.newBuilder().programName(Main::class.java.getName())
+ .addObject(defaultArguments)
+ .addObject(command)
+
+ /** Default arguments that may always be provided. */
+ private class DefaultArguments {
+ /** Shows the help message. */
+ @Parameter(names = ["--help"], help = true, description = "Shows all available command line arguments.")
+ val help = false
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/PreMain.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/PreMain.kt
new file mode 100644
index 000000000..37f157af1
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/PreMain.kt
@@ -0,0 +1,306 @@
+package com.teamscale.jacoco.agent
+
+import com.teamscale.client.FileSystemUtils
+import com.teamscale.client.HttpUtils
+import com.teamscale.client.StringUtils
+import com.teamscale.jacoco.agent.configuration.AgentOptionReceiveException
+import com.teamscale.jacoco.agent.logging.DebugLogDirectoryPropertyDefiner
+import com.teamscale.jacoco.agent.logging.LogDirectoryPropertyDefiner
+import com.teamscale.jacoco.agent.logging.LogToTeamscaleAppender
+import com.teamscale.jacoco.agent.logging.LoggingUtils
+import com.teamscale.jacoco.agent.options.AgentOptionParseException
+import com.teamscale.jacoco.agent.options.AgentOptions
+import com.teamscale.jacoco.agent.options.AgentOptionsParser
+import com.teamscale.jacoco.agent.options.FilePatternResolver
+import com.teamscale.jacoco.agent.options.JacocoAgentOptionsBuilder
+import com.teamscale.jacoco.agent.options.TeamscalePropertiesUtils
+import com.teamscale.jacoco.agent.testimpact.TestwiseCoverageAgent
+import com.teamscale.jacoco.agent.upload.UploaderException
+import com.teamscale.jacoco.agent.util.AgentUtils
+import com.teamscale.report.util.ILogger
+import java.io.IOException
+import java.lang.instrument.Instrumentation
+import java.lang.management.ManagementFactory
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import kotlin.use
+
+/** Container class for the premain entry point for the agent. */
+object PreMain {
+ private lateinit var loggingResources: LoggingUtils.LoggingResources
+
+ /**
+ * System property that we use to prevent this agent from being attached to the same VM twice. This can happen if
+ * the agent is registered via multiple JVM environment variables and/or the command line at the same time.
+ */
+ private const val LOCKING_SYSTEM_PROPERTY = "TEAMSCALE_JAVA_PROFILER_ATTACHED"
+
+ /**
+ * Environment variable from which to read the config ID to use. This is an ID for a profiler configuration that is
+ * stored in Teamscale.
+ */
+ private const val CONFIG_ID_ENVIRONMENT_VARIABLE = "TEAMSCALE_JAVA_PROFILER_CONFIG_ID"
+
+ /** Environment variable from which to read the config file to use. */
+ private const val CONFIG_FILE_ENVIRONMENT_VARIABLE = "TEAMSCALE_JAVA_PROFILER_CONFIG_FILE"
+
+ /** Environment variable from which to read the Teamscale access token. */
+ private const val ACCESS_TOKEN_ENVIRONMENT_VARIABLE = "TEAMSCALE_ACCESS_TOKEN"
+
+ /**
+ * Entry point for the agent, called by the JVM.
+ */
+ @JvmStatic
+ @Throws(Exception::class)
+ fun premain(options: String?, instrumentation: Instrumentation?) {
+ if (System.getProperty(LOCKING_SYSTEM_PROPERTY) != null) return
+ System.setProperty(LOCKING_SYSTEM_PROPERTY, "true")
+
+ val environmentConfigId = System.getenv(CONFIG_ID_ENVIRONMENT_VARIABLE)
+ val environmentConfigFile = System.getenv(CONFIG_FILE_ENVIRONMENT_VARIABLE)
+ if (StringUtils.isEmpty(options) && environmentConfigId == null && environmentConfigFile == null) {
+ // profiler was registered globally, and no config was set explicitly by the user, thus ignore this process
+ // and don't profile anything
+ return
+ }
+
+ var agentOptions: AgentOptions? = null
+ try {
+ val parseResult = getAndApplyAgentOptions(
+ options, environmentConfigId, environmentConfigFile
+ )
+ agentOptions = parseResult.first
+
+ // After parsing everything and configuring logging, we now
+ // can throw the caught exceptions.
+ parseResult.second?.forEach { exception ->
+ throw exception
+ }
+ } catch (e: AgentOptionParseException) {
+ LoggingUtils.loggerContext.getLogger(PreMain::class.java).error(e.message, e)
+
+ // Flush logs to Teamscale, if configured.
+ closeLoggingResources()
+
+ // Unregister the profiler from Teamscale.
+ agentOptions?.configurationViaTeamscale?.unregisterProfiler()
+
+ throw e
+ } catch (_: AgentOptionReceiveException) {
+ // When Teamscale is not available, we don't want to fail hard to still allow for testing even if no
+ // coverage is collected (see TS-33237)
+ return
+ }
+
+ val logger = LoggingUtils.getLogger(Agent::class.java)
+
+ logger.info("Teamscale Java profiler version ${AgentUtils.VERSION}")
+ logger.info("Starting JaCoCo's agent")
+ val agentBuilder = JacocoAgentOptionsBuilder(agentOptions)
+ JaCoCoPreMain.premain(agentBuilder.createJacocoAgentOptions(), instrumentation, logger)
+
+ agentOptions.configurationViaTeamscale?.startHeartbeatThreadAndRegisterShutdownHook()
+ createAgent(agentOptions, instrumentation).registerShutdownHook()
+ }
+
+ @Throws(AgentOptionParseException::class, IOException::class, AgentOptionReceiveException::class)
+ private fun getAndApplyAgentOptions(
+ options: String?,
+ environmentConfigId: String?,
+ environmentConfigFile: String?
+ ): Pair?> {
+ val delayedLogger = DelayedLogger()
+ val javaAgents = ManagementFactory.getRuntimeMXBean().inputArguments
+ .filter { it.contains("-javaagent") }
+ // We allow multiple instances of the teamscale-jacoco-agent as we ensure with the #LOCKING_SYSTEM_PROPERTY to only use it once
+ val differentAgents = javaAgents.filter { !it.contains("teamscale-jacoco-agent.jar") }
+
+ if (!differentAgents.isEmpty()) {
+ delayedLogger.warn(
+ "Using multiple java agents could interfere with coverage recording: ${
+ differentAgents.joinToString()
+ }"
+ )
+ }
+ if (!javaAgents.first().contains("teamscale-jacoco-agent.jar")) {
+ delayedLogger.warn("For best results consider registering the Teamscale Java Profiler first.")
+ }
+
+ val credentials = TeamscalePropertiesUtils.parseCredentials()
+ if (credentials == null) {
+ // As many users still don't use the installer based setup, this log message will be shown in almost every log.
+ // We use a debug log, as this message can be confusing for customers that think a teamscale.properties file is synonymous with a config file.
+ delayedLogger.debug(
+ "No explicit teamscale.properties file given. Looking for Teamscale credentials in a config file or via a command line argument. This is expected unless the installer based setup was used."
+ )
+ }
+
+ val environmentAccessToken = System.getenv(ACCESS_TOKEN_ENVIRONMENT_VARIABLE)
+
+ val parseResult: Pair>
+ val agentOptions: AgentOptions
+ try {
+ parseResult = AgentOptionsParser.parse(
+ options, environmentConfigId, environmentConfigFile, credentials, environmentAccessToken, delayedLogger
+ )
+ agentOptions = parseResult.first
+ } catch (e: AgentOptionParseException) {
+ initializeFallbackLogging(options, delayedLogger).use { _ ->
+ delayedLogger.errorAndStdErr("Failed to parse agent options: ${e.message}", e)
+ attemptLogAndThrow(delayedLogger)
+ throw e
+ }
+ } catch (e: AgentOptionReceiveException) {
+ initializeFallbackLogging(options, delayedLogger).use { _ ->
+ delayedLogger.errorAndStdErr("${e.message} The application should start up normally, but NO coverage will be collected! Check the log file for details.", e)
+ attemptLogAndThrow(delayedLogger)
+ throw e
+ }
+ }
+
+ initializeLogging(agentOptions, delayedLogger)
+ val logger = LoggingUtils.getLogger(Agent::class.java)
+ delayedLogger.logTo(logger)
+ HttpUtils.setShouldValidateSsl(agentOptions.shouldValidateSsl())
+
+ return parseResult
+ }
+
+ private fun attemptLogAndThrow(delayedLogger: DelayedLogger) {
+ // We perform actual logging output after writing to console to
+ // ensure the console is reached even in case of logging issues
+ // (see TS-23151). We use the Agent class here (same as below)
+ val logger = LoggingUtils.getLogger(Agent::class.java)
+ delayedLogger.logTo(logger)
+ }
+
+ /** Initializes logging during [premain] and also logs the log directory. */
+ @Throws(IOException::class)
+ private fun initializeLogging(agentOptions: AgentOptions, logger: DelayedLogger) {
+ if (agentOptions.isDebugLogging) {
+ initializeDebugLogging(agentOptions, logger)
+ } else {
+ loggingResources = LoggingUtils.initializeLogging(agentOptions.getLoggingConfig())
+ logger.info("Logging to ${LogDirectoryPropertyDefiner().getPropertyValue()}")
+ }
+
+ if (agentOptions.teamscaleServerOptions.isConfiguredForServerConnection) {
+ if (LogToTeamscaleAppender.addTeamscaleAppenderTo(LoggingUtils.loggerContext, agentOptions)) {
+ logger.info("Logs are being forwarded to Teamscale at ${agentOptions.teamscaleServerOptions.url}")
+ }
+ }
+ }
+
+ /** Closes the opened logging contexts. */
+ fun closeLoggingResources() {
+ loggingResources.close()
+ }
+
+ /**
+ * Returns in instance of the agent that was configured. Either an agent with interval based line-coverage dump or
+ * the HTTP server is used.
+ */
+ @Throws(UploaderException::class, IOException::class)
+ private fun createAgent(
+ agentOptions: AgentOptions,
+ instrumentation: Instrumentation?
+ ): AgentBase = if (agentOptions.useTestwiseCoverageMode()) {
+ TestwiseCoverageAgent.create(agentOptions)
+ } else {
+ Agent(agentOptions, instrumentation)
+ }
+
+ /**
+ * Initializes debug logging during [.premain] and also logs the log directory if
+ * given.
+ */
+ private fun initializeDebugLogging(agentOptions: AgentOptions, logger: DelayedLogger) {
+ loggingResources = LoggingUtils.initializeDebugLogging(agentOptions.getDebugLogDirectory())
+ val logDirectory = Paths.get(DebugLogDirectoryPropertyDefiner().getPropertyValue())
+ if (FileSystemUtils.isValidPath(logDirectory.toString()) && Files.isWritable(logDirectory)) {
+ logger.info("Logging to $logDirectory")
+ } else {
+ logger.warn("Could not create $logDirectory. Logging to console only.")
+ }
+ }
+
+ /**
+ * Initializes fallback logging in case of an error during the parsing of the options to
+ * [premain] (see TS-23151). This tries to extract the logging configuration and use
+ * this and falls back to the default logger.
+ */
+ private fun initializeFallbackLogging(
+ premainOptions: String?,
+ delayedLogger: DelayedLogger
+ ): LoggingUtils.LoggingResources? {
+ if (premainOptions == null) {
+ return LoggingUtils.initializeDefaultLogging()
+ }
+ premainOptions
+ .split(",".toRegex())
+ .dropLastWhile { it.isEmpty() }
+ .forEach { optionPart ->
+ if (optionPart.startsWith(AgentOptionsParser.DEBUG + "=")) {
+ val value = optionPart.split("=".toRegex(), limit = 2)[1]
+ val debugDisabled = value.equals("false", ignoreCase = true)
+ val debugEnabled = value.equals("true", ignoreCase = true)
+ if (debugDisabled) return@forEach
+ var debugLogDirectory: Path? = null
+ if (!value.isEmpty() && !debugEnabled) {
+ debugLogDirectory = Paths.get(value)
+ }
+ return LoggingUtils.initializeDebugLogging(debugLogDirectory)
+ }
+ if (optionPart.startsWith(AgentOptionsParser.LOGGING_CONFIG_OPTION + "=")) {
+ return createFallbackLoggerFromConfig(
+ optionPart.split("=".toRegex(), limit = 2)[1],
+ delayedLogger
+ )
+ }
+
+ if (optionPart.startsWith(AgentOptionsParser.CONFIG_FILE_OPTION + "=")) {
+ val configFileValue = optionPart.split("=".toRegex(), limit = 2)[1]
+ var loggingConfigLine: String? = null
+ try {
+ val configFile = FilePatternResolver(delayedLogger).parsePath(
+ AgentOptionsParser.CONFIG_FILE_OPTION, configFileValue
+ ).toFile()
+ loggingConfigLine = FileSystemUtils.readLinesUTF8(configFile)
+ .firstOrNull { it.startsWith(AgentOptionsParser.LOGGING_CONFIG_OPTION + "=") }
+ } catch (e: IOException) {
+ delayedLogger.error("Failed to load configuration from $configFileValue: ${e.message}", e)
+ }
+ loggingConfigLine?.let { config ->
+ return createFallbackLoggerFromConfig(
+ config.split("=".toRegex(), limit = 2)[1], delayedLogger
+ )
+ }
+ }
+ }
+
+ return LoggingUtils.initializeDefaultLogging()
+ }
+
+ /** Creates a fallback logger using the given config file. */
+ private fun createFallbackLoggerFromConfig(
+ configLocation: String,
+ delayedLogger: ILogger
+ ): LoggingUtils.LoggingResources {
+ try {
+ return LoggingUtils.initializeLogging(
+ FilePatternResolver(delayedLogger).parsePath(
+ AgentOptionsParser.LOGGING_CONFIG_OPTION,
+ configLocation
+ )
+ )
+ } catch (e: IOException) {
+ val message = "Failed to load log configuration from location $configLocation: ${e.message}"
+ delayedLogger.error(message, e)
+ // output the message to console as well, as this might
+ // otherwise not make it to the user
+ System.err.println(message)
+ return LoggingUtils.initializeDefaultLogging()
+ }
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/ResourceBase.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/ResourceBase.kt
new file mode 100644
index 000000000..fd4bb77cc
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/ResourceBase.kt
@@ -0,0 +1,141 @@
+package com.teamscale.jacoco.agent
+
+import com.teamscale.client.CommitDescriptor
+import com.teamscale.client.StringUtils
+import com.teamscale.client.TeamscaleServer
+import com.teamscale.jacoco.agent.logging.LoggingUtils
+import com.teamscale.report.testwise.model.RevisionInfo
+import org.jetbrains.annotations.Contract
+import org.slf4j.Logger
+import java.util.Optional
+import javax.ws.rs.BadRequestException
+import javax.ws.rs.GET
+import javax.ws.rs.PUT
+import javax.ws.rs.Path
+import javax.ws.rs.Produces
+import javax.ws.rs.core.MediaType
+import javax.ws.rs.core.Response
+
+/**
+ * The resource of the Jersey + Jetty http server holding all the endpoints specific for the [AgentBase].
+ */
+abstract class ResourceBase {
+ /** The logger. */
+ @JvmField
+ protected val logger: Logger = LoggingUtils.getLogger(this)
+
+ companion object {
+ /**
+ * The agentBase inject via [AgentResource.setAgent] or
+ * [com.teamscale.jacoco.agent.testimpact.TestwiseCoverageResource.setAgent].
+ */
+ @JvmStatic
+ protected lateinit var agentBase: AgentBase
+ }
+
+ @get:Path("/partition")
+ @get:GET
+ val partition: String
+ /** Returns the partition for the Teamscale upload. */
+ get() = agentBase.options.teamscaleServerOptions.partition.orEmpty()
+
+ @get:Path("/message")
+ @get:GET
+ val message: String
+ /** Returns the upload message for the Teamscale upload. */
+ get() = agentBase.options.teamscaleServerOptions.message.orEmpty()
+
+ @get:Produces(MediaType.APPLICATION_JSON)
+ @get:Path("/revision")
+ @get:GET
+ val revision: RevisionInfo
+ /** Returns revision information for the Teamscale upload. */
+ get() = revisionInfo
+
+ @get:Produces(MediaType.APPLICATION_JSON)
+ @get:Path("/commit")
+ @get:GET
+ val commit: RevisionInfo
+ /** Returns revision information for the Teamscale upload. */
+ get() = revisionInfo
+
+ /** Handles setting the partition name. */
+ @PUT
+ @Path("/partition")
+ fun setPartition(partitionString: String): Response {
+ val partition = StringUtils.removeDoubleQuotes(partitionString)
+ if (partition.isEmpty()) {
+ handleBadRequest("The new partition name is missing in the request body! Please add it as plain text.")
+ }
+
+ logger.debug("Changing partition name to $partition")
+ agentBase.dumpReport()
+ agentBase.controller.sessionId = partition
+ agentBase.options.teamscaleServerOptions.partition = partition
+ return Response.noContent().build()
+ }
+
+ /** Handles setting the upload message. */
+ @PUT
+ @Path("/message")
+ fun setMessage(messageString: String): Response {
+ val message = StringUtils.removeDoubleQuotes(messageString)
+ if (message.isEmpty()) {
+ handleBadRequest("The new message is missing in the request body! Please add it as plain text.")
+ }
+
+ agentBase.dumpReport()
+ logger.debug("Changing message to $message")
+ agentBase.options.teamscaleServerOptions.message = message
+
+ return Response.noContent().build()
+ }
+
+ /** Handles setting the revision. */
+ @PUT
+ @Path("/revision")
+ fun setRevision(revisionString: String): Response {
+ val revision = StringUtils.removeDoubleQuotes(revisionString)
+ if (revision.isEmpty()) {
+ handleBadRequest("The new revision name is missing in the request body! Please add it as plain text.")
+ }
+
+ agentBase.dumpReport()
+ logger.debug("Changing revision name to $revision")
+ agentBase.options.teamscaleServerOptions.revision = revision
+
+ return Response.noContent().build()
+ }
+
+ /** Handles setting the upload commit. */
+ @PUT
+ @Path("/commit")
+ fun setCommit(commitString: String): Response {
+ val commit = StringUtils.removeDoubleQuotes(commitString)
+ if (commit.isEmpty()) {
+ handleBadRequest("The new upload commit is missing in the request body! Please add it as plain text.")
+ }
+
+ agentBase.dumpReport()
+ agentBase.options.teamscaleServerOptions.commit = CommitDescriptor.parse(commit)
+
+ return Response.noContent().build()
+ }
+
+ private val revisionInfo: RevisionInfo
+ /** Returns revision information for the Teamscale upload. */
+ get() {
+ val server = agentBase.options.teamscaleServerOptions
+ return RevisionInfo(server.commit, server.revision)
+ }
+
+ /**
+ * Handles bad requests to the endpoints.
+ */
+ @Contract(value = "_ -> fail")
+ @Throws(BadRequestException::class)
+ protected fun handleBadRequest(message: String?) {
+ logger.error(message)
+ throw BadRequestException(message)
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/commandline/ICommand.java b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commandline/ICommand.kt
similarity index 77%
rename from agent/src/main/java/com/teamscale/jacoco/agent/commandline/ICommand.java
rename to agent/src/main/kotlin/com/teamscale/jacoco/agent/commandline/ICommand.kt
index 4ce2fa697..5bbc9b7cd 100644
--- a/agent/src/main/java/com/teamscale/jacoco/agent/commandline/ICommand.java
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commandline/ICommand.kt
@@ -3,27 +3,26 @@
| Copyright (c) 2009-2017 CQSE GmbH |
| |
+-------------------------------------------------------------------------*/
-package com.teamscale.jacoco.agent.commandline;
+package com.teamscale.jacoco.agent.commandline
-import com.teamscale.jacoco.agent.options.AgentOptionParseException;
-
-import java.io.IOException;
+import com.teamscale.jacoco.agent.options.AgentOptionParseException
+import java.io.IOException
/**
* Interface for commands: argument parsing and execution.
*/
-public interface ICommand {
-
+interface ICommand {
/**
* Makes sure the arguments are valid. Must return all detected problems in the
* form of a user-visible message.
*/
- Validator validate() throws AgentOptionParseException, IOException;
+ @Throws(AgentOptionParseException::class, IOException::class)
+ fun validate(): Validator
/**
* Runs the implementation of the command. May throw an exception to indicate
* abnormal termination of the program.
*/
- void run() throws Exception;
-
+ @Throws(Exception::class)
+ fun run()
}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/commandline/Validator.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commandline/Validator.kt
new file mode 100644
index 000000000..6f520dc7d
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commandline/Validator.kt
@@ -0,0 +1,61 @@
+/*-------------------------------------------------------------------------+
+| |
+| Copyright (c) 2009-2017 CQSE GmbH |
+| |
++-------------------------------------------------------------------------*/
+package com.teamscale.jacoco.agent.commandline
+
+import com.teamscale.client.StringUtils
+import com.teamscale.jacoco.agent.util.Assertions
+
+/**
+ * Helper class to allow for multiple validations to occur.
+ */
+class Validator {
+ /** The found validation problems in the form of error messages for the user. */
+ private val messages = mutableListOf()
+
+ /** Runs the given validation routine. */
+ fun ensure(validation: ExceptionBasedValidation) {
+ try {
+ validation.validate()
+ } catch (e: Exception) {
+ e.message?.let { messages.add(it) }
+ } catch (e: AssertionError) {
+ e.message?.let { messages.add(it) }
+ }
+ }
+
+ /**
+ * Interface for a validation routine that throws an exception when it fails.
+ */
+ fun interface ExceptionBasedValidation {
+ /**
+ * Throws an [Exception] or [AssertionError] if the validation fails.
+ */
+ @Throws(Exception::class, AssertionError::class)
+ fun validate()
+ }
+
+ /**
+ * Checks that the given condition is `true` or adds the given error message.
+ */
+ fun isTrue(condition: Boolean, message: String?) {
+ ensure { Assertions.isTrue(condition, message) }
+ }
+
+ /**
+ * Checks that the given condition is `false` or adds the given error message.
+ */
+ fun isFalse(condition: Boolean, message: String?) {
+ ensure { Assertions.isFalse(condition, message) }
+ }
+
+ val isValid: Boolean
+ /** Returns `true` if the validation succeeded. */
+ get() = messages.isEmpty()
+
+ val errorMessage: String
+ /** Returns an error message with all validation problems that were found. */
+ get() = "- ${messages.joinToString("${StringUtils.LINE_FEED}- ")}"
+}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/CommitInfo.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/CommitInfo.kt
new file mode 100644
index 000000000..d66014435
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/CommitInfo.kt
@@ -0,0 +1,28 @@
+package com.teamscale.jacoco.agent.commit_resolution.git_properties
+
+import com.teamscale.client.CommitDescriptor
+import com.teamscale.client.StringUtils.isEmpty
+import java.util.*
+
+/** Hold information regarding a commit. */
+data class CommitInfo(
+ /** The revision information (git hash). */
+ @JvmField var revision: String?,
+ /** The commit descriptor. */
+ @JvmField var commit: CommitDescriptor?
+) {
+ /**
+ * If the commit property is set via the `teamscale.commit.branch` and `teamscale.commit.time`
+ * properties in a git.properties file, this should be preferred to the revision. For details see [TS-38561](https://cqse.atlassian.net/browse/TS-38561).
+ */
+ @JvmField
+ var preferCommitDescriptorOverRevision: Boolean = false
+
+ override fun toString() = "$commit/$revision"
+
+ /**
+ * Returns true if one of or both, revision and commit, are set
+ */
+ val isEmpty: Boolean
+ get() = revision.isNullOrEmpty() && commit == null
+}
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
new file mode 100644
index 000000000..935272d8e
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitMultiProjectPropertiesLocator.kt
@@ -0,0 +1,95 @@
+package com.teamscale.jacoco.agent.commit_resolution.git_properties
+
+import com.teamscale.jacoco.agent.logging.LoggingUtils.getLogger
+import com.teamscale.jacoco.agent.upload.teamscale.DelayedTeamscaleMultiProjectUploader
+import com.teamscale.jacoco.agent.util.DaemonThreadFactory
+import org.jetbrains.annotations.VisibleForTesting
+import java.io.File
+import java.io.IOException
+import java.time.format.DateTimeFormatter
+import java.util.concurrent.Executor
+import java.util.concurrent.Executors
+
+/**
+ * Searches a Jar/War/Ear/... file for a git.properties file in order to enable upload for the commit described therein,
+ * e.g. to Teamscale, via a [DelayedTeamscaleMultiProjectUploader]. Specifically, this searches for the
+ * 'teamscale.project' property specified in each of the discovered 'git.properties' files.
+ */
+class GitMultiProjectPropertiesLocator(
+ private val uploader: DelayedTeamscaleMultiProjectUploader,
+ private val executor: Executor,
+ private val recursiveSearch: Boolean,
+ private val gitPropertiesCommitTimeFormat: DateTimeFormatter?
+) : IGitPropertiesLocator {
+ private val logger = getLogger(this)
+
+ constructor(
+ uploader: DelayedTeamscaleMultiProjectUploader,
+ recursiveSearch: Boolean,
+ gitPropertiesCommitTimeFormat: DateTimeFormatter?
+ ) : this(
+ uploader, Executors.newSingleThreadExecutor(
+ DaemonThreadFactory(
+ GitMultiProjectPropertiesLocator::class.java,
+ "git.properties Jar scanner thread"
+ )
+ ), recursiveSearch, gitPropertiesCommitTimeFormat
+ )
+
+ /**
+ * Asynchronously searches the given jar file for git.properties files and adds a corresponding uploader to the
+ * multi-project uploader.
+ */
+ override fun searchFileForGitPropertiesAsync(file: File, isJarFile: Boolean) {
+ executor.execute { searchFile(file, isJarFile) }
+ }
+
+ /**
+ * Synchronously searches the given jar file for git.properties files and adds a corresponding uploader to the
+ * multi-project uploader.
+ */
+ @VisibleForTesting
+ fun searchFile(file: File, isJarFile: Boolean) {
+ logger.debug("Searching file {} for multiple git.properties", file.toString())
+ try {
+ val projectAndCommits = GitPropertiesLocatorUtils.getProjectRevisionsFromGitProperties(
+ file, isJarFile, recursiveSearch, gitPropertiesCommitTimeFormat
+ )
+ if (projectAndCommits.isEmpty()) {
+ logger.debug("No git.properties file found in {}", file)
+ return
+ }
+
+ projectAndCommits.forEach { projectAndCommit ->
+ // this code only runs when 'teamscale-project' is not given via the agent properties,
+ // i.e., a multi-project upload is being attempted.
+ // Therefore, we expect to find both the project (teamscale.project) and the revision
+ // (git.commit.id) in the git.properties file.
+ if (projectAndCommit.project == null || projectAndCommit.commitInfo == null) {
+ logger.debug(
+ "Found inconsistent git.properties file: the git.properties file in {} either does not specify the" +
+ " Teamscale project ({}) property, or does not specify the commit " +
+ "({}, {} + {}, or {} + {})." +
+ " Will skip this git.properties file and try to continue with the other ones that were found during discovery.",
+ file, GitPropertiesLocatorUtils.GIT_PROPERTIES_TEAMSCALE_PROJECT,
+ GitPropertiesLocatorUtils.GIT_PROPERTIES_GIT_COMMIT_ID,
+ GitPropertiesLocatorUtils.GIT_PROPERTIES_GIT_BRANCH,
+ GitPropertiesLocatorUtils.GIT_PROPERTIES_GIT_COMMIT_TIME,
+ GitPropertiesLocatorUtils.GIT_PROPERTIES_TEAMSCALE_COMMIT_BRANCH,
+ GitPropertiesLocatorUtils.GIT_PROPERTIES_TEAMSCALE_COMMIT_TIME
+ )
+ return@forEach
+ }
+ logger.debug(
+ "Found git.properties file in {} and found Teamscale project {} and revision {}", file,
+ projectAndCommit.project, projectAndCommit.commitInfo
+ )
+ uploader.addTeamscaleProjectAndCommit(file, projectAndCommit)
+ }
+ } catch (e: IOException) {
+ logger.error("Error during asynchronous search for git.properties in {}", file, e)
+ } catch (e: InvalidGitPropertiesException) {
+ logger.error("Error during asynchronous search for git.properties in {}", 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
new file mode 100644
index 000000000..107f63a93
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitPropertiesLocatingTransformer.kt
@@ -0,0 +1,80 @@
+package com.teamscale.jacoco.agent.commit_resolution.git_properties
+
+import com.teamscale.jacoco.agent.logging.LoggingUtils.getLogger
+import com.teamscale.report.util.ClasspathWildcardIncludeFilter
+import java.io.File
+import java.lang.instrument.ClassFileTransformer
+import java.security.ProtectionDomain
+import java.util.concurrent.ConcurrentSkipListSet
+
+/**
+ * [ClassFileTransformer] that doesn't change the loaded classes but searches their corresponding Jar/War/Ear/...
+ * files for a git.properties file.
+ */
+class GitPropertiesLocatingTransformer(
+ private val locator: IGitPropertiesLocator,
+ private val locationIncludeFilter: ClasspathWildcardIncludeFilter
+) : ClassFileTransformer {
+ private val logger = getLogger(this)
+ private val seenJars = ConcurrentSkipListSet()
+
+ override fun transform(
+ classLoader: ClassLoader?,
+ className: String,
+ aClass: Class<*>?,
+ protectionDomain: ProtectionDomain?,
+ classFileContent: ByteArray?
+ ): ByteArray? {
+ if (protectionDomain == null) {
+ // happens for e.g. java.lang. We can ignore these classes
+ return null
+ }
+
+ if (className.isEmpty() || !locationIncludeFilter.isIncluded(className)) {
+ // only search in jar files of included classes
+ return null
+ }
+
+ try {
+ val codeSource = protectionDomain.codeSource
+ if (codeSource == null || codeSource.location == null) {
+ // unknown when this can happen, we suspect when code is generated at runtime
+ // but there's nothing else we can do here in either case.
+ // codeSource.getLocation() is null e.g. when executing Pixelitor with Java14 for class sun/reflect/misc/Trampoline
+ logger.debug(
+ "Could not locate code source for class {}. Skipping git.properties search for this class",
+ className
+ )
+ return null
+ }
+
+ val jarOrClassFolderUrl = codeSource.location
+ val searchRoot = GitPropertiesLocatorUtils.extractGitPropertiesSearchRoot(
+ jarOrClassFolderUrl
+ )
+ if (searchRoot == null || searchRoot.first == null) {
+ logger.warn(
+ "Not searching location for git.properties with unknown protocol or extension {}." +
+ " If this location contains your git.properties, please report this warning as a" +
+ " bug to CQSE. In that case, auto-discovery of git.properties will not work.",
+ jarOrClassFolderUrl
+ )
+ return null
+ }
+
+ if (hasLocationAlreadyBeenSearched(searchRoot.first!!)) {
+ return null
+ }
+
+ logger.debug("Scheduling asynchronous search for git.properties in {}", searchRoot)
+ locator.searchFileForGitPropertiesAsync(searchRoot.first, searchRoot.second!!)
+ } 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)
+ }
+ return null
+ }
+
+ private fun hasLocationAlreadyBeenSearched(location: File) = !seenJars.add(location.toString())
+}
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
new file mode 100644
index 000000000..78a5d7ffb
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitPropertiesLocatorUtils.kt
@@ -0,0 +1,511 @@
+package com.teamscale.jacoco.agent.commit_resolution.git_properties
+
+import com.teamscale.client.CommitDescriptor
+import com.teamscale.client.FileSystemUtils.listFilesRecursively
+import com.teamscale.client.StringUtils.endsWithOneOf
+import com.teamscale.client.StringUtils.isEmpty
+import com.teamscale.jacoco.agent.options.ProjectAndCommit
+import com.teamscale.report.util.BashFileSkippingInputStream
+import java.io.File
+import java.io.IOException
+import java.lang.reflect.InvocationTargetException
+import java.net.URISyntaxException
+import java.net.URL
+import java.nio.file.Files
+import java.nio.file.Paths
+import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
+import java.time.format.DateTimeFormatterBuilder
+import java.time.format.DateTimeParseException
+import java.util.*
+import java.util.jar.JarEntry
+import java.util.jar.JarInputStream
+import java.util.regex.Pattern
+
+/** Utility methods to extract certain properties from git.properties files in archives and folders. */
+object GitPropertiesLocatorUtils {
+ /** Name of the git.properties file. */
+ const val GIT_PROPERTIES_FILE_NAME: String = "git.properties"
+
+ /** The git.properties key that holds the commit time. */
+ const val GIT_PROPERTIES_GIT_COMMIT_TIME: String = "git.commit.time"
+
+ /** The git.properties key that holds the commit branch. */
+ const val GIT_PROPERTIES_GIT_BRANCH: String = "git.branch"
+
+ /** The git.properties key that holds the commit hash. */
+ const val GIT_PROPERTIES_GIT_COMMIT_ID: String = "git.commit.id"
+
+ /**
+ * Alternative git.properties key that might also hold the commit hash, depending on the Maven git-commit-id plugin
+ * configuration.
+ */
+ const val GIT_PROPERTIES_GIT_COMMIT_ID_FULL: String = "git.commit.id.full"
+
+ /**
+ * You can provide a teamscale timestamp in git.properties files to overwrite the revision. See [TS-38561](https://cqse.atlassian.net/browse/TS-38561).
+ */
+ const val GIT_PROPERTIES_TEAMSCALE_COMMIT_BRANCH: String = "teamscale.commit.branch"
+
+ /**
+ * You can provide a teamscale timestamp in git.properties files to overwrite the revision. See [TS-38561](https://cqse.atlassian.net/browse/TS-38561).
+ */
+ const val GIT_PROPERTIES_TEAMSCALE_COMMIT_TIME: String = "teamscale.commit.time"
+
+ /** The git.properties key that holds the Teamscale project name. */
+ const val GIT_PROPERTIES_TEAMSCALE_PROJECT: String = "teamscale.project"
+
+ /** Matches the path to the jar file in a jar:file: URL in regex group 1. */
+ private val JAR_URL_REGEX: Pattern = Pattern.compile(
+ "jar:(?:file|nested):(.*?)!.*",
+ Pattern.CASE_INSENSITIVE
+ )
+
+ private val NESTED_JAR_REGEX: Pattern = Pattern.compile(
+ "[jwea]ar:file:(.*?)\\*(.*)",
+ Pattern.CASE_INSENSITIVE
+ )
+
+ /**
+ * Defined in [GitCommitIdMojo](https://github.com/git-commit-id/git-commit-id-maven-plugin/blob/ac05b16dfdcc2aebfa45ad3af4acf1254accffa3/src/main/java/pl/project13/maven/git/GitCommitIdMojo.java#L522)
+ */
+ private const val GIT_PROPERTIES_DEFAULT_MAVEN_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssXXX"
+
+ /**
+ * Defined in [GitPropertiesPlugin](https://github.com/n0mer/gradle-git-properties/blob/bb1c3353bb570495644b6c6c75e211296a8354fc/src/main/groovy/com/gorylenko/GitPropertiesPlugin.groovy#L68)
+ */
+ private const val GIT_PROPERTIES_DEFAULT_GRADLE_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ"
+
+ /**
+ * Reads the git SHA1 and branch and timestamp from the given jar file's git.properties and builds a commit
+ * descriptor out of it. If no git.properties file can be found, returns null.
+ *
+ * @throws IOException If reading the jar file fails.
+ * @throws InvalidGitPropertiesException If a git.properties file is found but it is malformed.
+ */
+ @JvmStatic
+ @Throws(IOException::class, InvalidGitPropertiesException::class)
+ fun getCommitInfoFromGitProperties(
+ file: File,
+ isJarFile: Boolean,
+ recursiveSearch: Boolean,
+ gitPropertiesCommitTimeFormat: DateTimeFormatter?
+ ) = findGitPropertiesInFile(file, isJarFile, recursiveSearch).map { entryWithProperties ->
+ getCommitInfoFromGitProperties(
+ entryWithProperties.second, entryWithProperties.first, file,
+ gitPropertiesCommitTimeFormat
+ )
+ }
+
+ /**
+ * Tries to extract a file system path to a search root for the git.properties search. A search root is either a
+ * file system folder or a Jar file. If no such path can be extracted, returns null.
+ *
+ * @throws URISyntaxException under certain circumstances if parsing the URL fails. This should be treated the same
+ * as a null search result but the exception is preserved so it can be logged.
+ */
+ @JvmStatic
+ @Throws(
+ URISyntaxException::class,
+ IOException::class,
+ NoSuchMethodException::class,
+ IllegalAccessException::class,
+ InvocationTargetException::class
+ )
+ fun extractGitPropertiesSearchRoot(
+ jarOrClassFolderUrl: URL
+ ): Pair? {
+ val protocol = jarOrClassFolderUrl.protocol.lowercase(Locale.getDefault())
+ when (protocol) {
+ "file" -> {
+ val jarOrClassFolderFile = File(jarOrClassFolderUrl.toURI())
+ if (jarOrClassFolderFile.isDirectory() || isJarLikeFile(jarOrClassFolderUrl.path)) {
+ return jarOrClassFolderFile to !jarOrClassFolderFile.isDirectory()
+ }
+ }
+
+ "jar" -> {
+ // Used e.g. by Spring Boot. Example: jar:file:/home/k/demo.jar!/BOOT-INF/classes!/
+ val jarMatcher = JAR_URL_REGEX.matcher(jarOrClassFolderUrl.toString())
+ if (jarMatcher.matches()) {
+ return File(jarMatcher.group(1)) to true
+ }
+ // Used by some web applications and potentially fat jars.
+ // Example: war:file:/Users/example/apache-tomcat/webapps/demo.war*/WEB-INF/lib/demoLib-1.0-SNAPSHOT.jar
+ val nestedMatcher = NESTED_JAR_REGEX.matcher(jarOrClassFolderUrl.toString())
+ if (nestedMatcher.matches()) {
+ return File(nestedMatcher.group(1)) to true
+ }
+ }
+
+ "war", "ear" -> {
+ val nestedMatcher = NESTED_JAR_REGEX.matcher(jarOrClassFolderUrl.toString())
+ if (nestedMatcher.matches()) {
+ return File(nestedMatcher.group(1)) to true
+ }
+ }
+
+ "vfs" -> return getVfsContentFolder(jarOrClassFolderUrl)
+ else -> return null
+ }
+ return null
+ }
+
+ /**
+ * VFS (Virtual File System) protocol is used by JBoss EAP and Wildfly. Example of an URL:
+ * vfs:/content/helloworld.war/WEB-INF/classes
+ */
+ @Throws(
+ IOException::class,
+ NoSuchMethodException::class,
+ IllegalAccessException::class,
+ InvocationTargetException::class
+ )
+ private fun getVfsContentFolder(
+ jarOrClassFolderUrl: URL
+ ): Pair {
+ // we obtain the URL of a specific class file as input, e.g.,
+ // vfs:/content/helloworld.war/WEB-INF/classes
+ // Next, we try to extract the artefact URL from it, e.g., vfs:/content/helloworld.war
+ val artefactUrl = extractArtefactUrl(jarOrClassFolderUrl)
+
+ val virtualFile = URL(artefactUrl).openConnection().getContent()
+ val virtualFileClass: Class<*> = virtualFile.javaClass
+ // obtain the physical location of the class file. It is created on demand in /standalone/tmp/vfs
+ val getPhysicalFileMethod = virtualFileClass.getMethod("getPhysicalFile")
+ val file = getPhysicalFileMethod.invoke(virtualFile) as File
+ return file to !file.isDirectory()
+ }
+
+ /**
+ * Extracts the artefact URL (e.g., vfs:/content/helloworld.war/) from the full URL of the class file (e.g.,
+ * vfs:/content/helloworld.war/WEB-INF/classes).
+ */
+ private fun extractArtefactUrl(jarOrClassFolderUrl: URL): String {
+ val url = jarOrClassFolderUrl.path.lowercase(Locale.getDefault())
+ val pathSegments = url.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
+ val artefactUrlBuilder = StringBuilder("vfs:")
+ var segmentIdx = 0
+ while (segmentIdx < pathSegments.size) {
+ val segment = pathSegments[segmentIdx]
+ artefactUrlBuilder.append(segment)
+ artefactUrlBuilder.append("/")
+ if (isJarLikeFile(segment)) {
+ break
+ }
+ segmentIdx += 1
+ }
+ if (segmentIdx == pathSegments.size) {
+ return url
+ }
+ return artefactUrlBuilder.toString()
+ }
+
+ private fun isJarLikeFile(segment: String) = endsWithOneOf(
+ segment.lowercase(Locale.getDefault()), ".jar", ".war", ".ear", ".aar"
+ )
+
+ /**
+ * Reads the 'teamscale.project' property values and the git SHA1s or branch + timestamp from all git.properties
+ * files contained in the provided folder or archive file.
+ *
+ * @throws IOException If reading the jar file fails.
+ * @throws InvalidGitPropertiesException If a git.properties file is found but it is malformed.
+ */
+ @JvmStatic
+ @Throws(IOException::class, InvalidGitPropertiesException::class)
+ fun getProjectRevisionsFromGitProperties(
+ file: File, isJarFile: Boolean, recursiveSearch: Boolean,
+ gitPropertiesCommitTimeFormat: DateTimeFormatter?
+ ) = findGitPropertiesInFile(
+ file, isJarFile,
+ recursiveSearch
+ ).map { entryWithProperties ->
+ val commitInfo = getCommitInfoFromGitProperties(
+ entryWithProperties.second,
+ entryWithProperties.first, file, gitPropertiesCommitTimeFormat
+ )
+ val project = entryWithProperties.second.getProperty(GIT_PROPERTIES_TEAMSCALE_PROJECT)
+ if (commitInfo.isEmpty && isEmpty(project)) {
+ throw InvalidGitPropertiesException(
+ "No entry or empty value for both '$GIT_PROPERTIES_GIT_COMMIT_ID'/'$GIT_PROPERTIES_GIT_COMMIT_ID_FULL' and '$GIT_PROPERTIES_TEAMSCALE_PROJECT' in $file.\nContents of $GIT_PROPERTIES_FILE_NAME: ${entryWithProperties.second}"
+ )
+ }
+ ProjectAndCommit(project, commitInfo)
+ }
+
+ /**
+ * Returns pairs of paths to git.properties files and their parsed properties found in the provided folder or
+ * archive file. Nested jar files will also be searched recursively if specified.
+ */
+ @JvmStatic
+ @Throws(IOException::class)
+ fun findGitPropertiesInFile(
+ file: File, isJarFile: Boolean, recursiveSearch: Boolean
+ ): List> {
+ if (isJarFile) {
+ return findGitPropertiesInArchiveFile(file, recursiveSearch)
+ }
+ return findGitPropertiesInDirectoryFile(file, recursiveSearch)
+ }
+
+ /**
+ * Searches for git properties in jar/war/ear/aar files
+ */
+ @Throws(IOException::class)
+ private fun findGitPropertiesInArchiveFile(
+ file: File,
+ recursiveSearch: Boolean
+ ): List> {
+ try {
+ JarInputStream(
+ BashFileSkippingInputStream(Files.newInputStream(file.toPath()))
+ ).use { jarStream ->
+ return findGitPropertiesInArchive(jarStream, file.getName(), recursiveSearch)
+ }
+ } catch (e: IOException) {
+ throw IOException(
+ "Reading jar ${file.absolutePath} for obtaining commit descriptor from git.properties failed", e
+ )
+ }
+ }
+
+ /**
+ * Searches for git.properties file in the given folder
+ *
+ * @param recursiveSearch If enabled, git.properties files will also be searched in jar files
+ */
+ @Throws(IOException::class)
+ private fun findGitPropertiesInDirectoryFile(
+ directoryFile: File, recursiveSearch: Boolean
+ ): List> {
+ val result = findGitPropertiesInFolder(directoryFile).toMutableList()
+
+ if (recursiveSearch) {
+ result.addAll(findGitPropertiesInNestedJarFiles(directoryFile))
+ }
+
+ return result.toList()
+ }
+
+ /**
+ * Finds all jar files in the given folder and searches them recursively for git.properties
+ */
+ @Throws(IOException::class)
+ private fun findGitPropertiesInNestedJarFiles(directoryFile: File) =
+ listFilesRecursively(directoryFile) {
+ isJarLikeFile(it.getName())
+ }.flatMap { jarFile ->
+ val inputStream = JarInputStream(Files.newInputStream(jarFile.toPath()))
+ val relativeFilePath = "${directoryFile.getName()}${File.separator}" + directoryFile.toPath()
+ .relativize(jarFile.toPath())
+ findGitPropertiesInArchive(inputStream, relativeFilePath, true)
+ }
+
+ /**
+ * Searches for git.properties files in the given folder
+ */
+ @Throws(IOException::class)
+ private fun findGitPropertiesInFolder(directoryFile: File) =
+ listFilesRecursively(directoryFile) {
+ it.getName().equals(GIT_PROPERTIES_FILE_NAME, ignoreCase = true)
+ }.map { gitPropertiesFile ->
+ try {
+ Files.newInputStream(gitPropertiesFile.toPath()).use { inputStream ->
+ val gitProperties = Properties()
+ gitProperties.load(inputStream)
+ val relativeFilePath = "${directoryFile.getName()}${File.separator}" + directoryFile.toPath()
+ .relativize(gitPropertiesFile.toPath())
+ relativeFilePath to gitProperties
+ }
+ } catch (e: IOException) {
+ throw IOException(
+ "Reading directory ${gitPropertiesFile.absolutePath} for obtaining commit descriptor from git.properties failed", e
+ )
+ }
+ }
+
+ /**
+ * Returns pairs of paths to git.properties files and their parsed properties found in the provided JarInputStream.
+ * Nested jar files will also be searched recursively if specified.
+ */
+ @JvmStatic
+ @JvmOverloads
+ @Throws(IOException::class)
+ fun findGitPropertiesInArchive(
+ inputStream: JarInputStream,
+ archiveName: String?,
+ recursiveSearch: Boolean,
+ isRootArchive: Boolean = true // Added flag to prevent nested crashes
+ ): MutableList> {
+ val result = mutableListOf>()
+ var isEmpty = true
+
+ var entry = inputStream.nextJarEntry
+ while (entry != null) {
+ isEmpty = false
+ val fullEntryName = if (archiveName.isNullOrEmpty()) entry.name else "$archiveName/${entry.name}"
+ val fileName = entry.name.substringAfterLast('/')
+
+ if (fileName.equals(GIT_PROPERTIES_FILE_NAME, ignoreCase = true)) {
+ val gitProperties = Properties().apply { load(inputStream) }
+ result.add(fullEntryName to gitProperties)
+
+ } else if (recursiveSearch && isJarLikeFile(entry.name)) {
+ val nestedJarStream = JarInputStream(inputStream)
+ result.addAll(
+ findGitPropertiesInArchive(nestedJarStream, fullEntryName,
+ recursiveSearch = true,
+ isRootArchive = false
+ )
+ )
+ }
+ entry = inputStream.nextJarEntry
+ }
+
+ if (isEmpty && isRootArchive) {
+ throw IOException("No entries in Jar file $archiveName. Is this a valid jar file?. If so, please report to CQSE.")
+ }
+
+ return result
+ }
+
+ /**
+ * Returns the CommitInfo (revision and branch + timestmap) from a git properties file. The revision can be either
+ * in [.GIT_PROPERTIES_GIT_COMMIT_ID] or [.GIT_PROPERTIES_GIT_COMMIT_ID_FULL]. The branch and timestamp
+ * in [.GIT_PROPERTIES_GIT_BRANCH] + [.GIT_PROPERTIES_GIT_COMMIT_TIME] or in
+ * [.GIT_PROPERTIES_TEAMSCALE_COMMIT_BRANCH] + [.GIT_PROPERTIES_TEAMSCALE_COMMIT_TIME]. By default,
+ * times will be parsed with [.GIT_PROPERTIES_DEFAULT_GRADLE_DATE_FORMAT] and
+ * [.GIT_PROPERTIES_DEFAULT_MAVEN_DATE_FORMAT]. An additional format can be given with
+ * `dateTimeFormatter`
+ */
+ @JvmStatic
+ @Throws(InvalidGitPropertiesException::class)
+ fun getCommitInfoFromGitProperties(
+ gitProperties: Properties, entryName: String?, jarFile: File?,
+ additionalDateTimeFormatter: DateTimeFormatter?
+ ): CommitInfo {
+ val dateTimeFormatter = createDateTimeFormatter(additionalDateTimeFormatter)
+
+ // Get Revision
+ val revision = getRevisionFromGitProperties(gitProperties)
+
+ // Get branch and timestamp from git.commit.branch and git.commit.id
+ var commitDescriptor = getCommitDescriptorFromDefaultGitPropertyValues(
+ gitProperties, entryName,
+ jarFile, dateTimeFormatter
+ )
+ // When read from these properties, we should prefer to upload to the revision
+ var preferCommitDescriptorOverRevision = false
+
+
+ // Get branch and timestamp from teamscale.commit.branch and teamscale.commit.time (TS-38561)
+ val teamscaleTimestampBasedCommitDescriptor = getCommitDescriptorFromTeamscaleTimestampProperty(
+ gitProperties, entryName, jarFile, dateTimeFormatter
+ )
+ if (teamscaleTimestampBasedCommitDescriptor != null) {
+ // In this case, as we specifically set this property, we should prefer branch and timestamp to the revision
+ preferCommitDescriptorOverRevision = true
+ commitDescriptor = teamscaleTimestampBasedCommitDescriptor
+ }
+
+ if (isEmpty(revision) && commitDescriptor == null) {
+ throw InvalidGitPropertiesException(
+ "No entry or invalid value for '" + GIT_PROPERTIES_GIT_COMMIT_ID + "', '" + GIT_PROPERTIES_GIT_COMMIT_ID_FULL +
+ "', '" + GIT_PROPERTIES_TEAMSCALE_COMMIT_BRANCH + "' and " + GIT_PROPERTIES_TEAMSCALE_COMMIT_TIME + "'\n" +
+ "Location: Entry '" + entryName + "' in jar file '" + jarFile + "'." +
+ "\nContents of " + GIT_PROPERTIES_FILE_NAME + ":\n" + gitProperties
+ )
+ }
+
+ val commitInfo = CommitInfo(revision, commitDescriptor)
+ commitInfo.preferCommitDescriptorOverRevision = preferCommitDescriptorOverRevision
+ return commitInfo
+ }
+
+ private fun createDateTimeFormatter(
+ additionalDateTimeFormatter: DateTimeFormatter?
+ ): DateTimeFormatter {
+ val defaultDateTimeFormatter = DateTimeFormatter.ofPattern(
+ String.format(
+ "[%s][%s]", GIT_PROPERTIES_DEFAULT_MAVEN_DATE_FORMAT,
+ GIT_PROPERTIES_DEFAULT_GRADLE_DATE_FORMAT
+ )
+ )
+ val builder = DateTimeFormatterBuilder().append(defaultDateTimeFormatter)
+ if (additionalDateTimeFormatter != null) {
+ builder.append(additionalDateTimeFormatter)
+ }
+ return builder.toFormatter()
+ }
+
+ private fun getRevisionFromGitProperties(gitProperties: Properties): String? {
+ var revision = gitProperties.getProperty(GIT_PROPERTIES_GIT_COMMIT_ID)
+ if (isEmpty(revision)) {
+ revision = gitProperties.getProperty(GIT_PROPERTIES_GIT_COMMIT_ID_FULL)
+ }
+ return revision
+ }
+
+ @Throws(InvalidGitPropertiesException::class)
+ private fun getCommitDescriptorFromTeamscaleTimestampProperty(
+ gitProperties: Properties,
+ entryName: String?,
+ jarFile: File?,
+ dateTimeFormatter: DateTimeFormatter
+ ): CommitDescriptor? {
+ val teamscaleCommitBranch = gitProperties.getProperty(GIT_PROPERTIES_TEAMSCALE_COMMIT_BRANCH)
+ val teamscaleCommitTime = gitProperties.getProperty(GIT_PROPERTIES_TEAMSCALE_COMMIT_TIME)
+
+ if (isEmpty(teamscaleCommitBranch) || isEmpty(teamscaleCommitTime)) {
+ return null
+ }
+
+ val teamscaleTimestampRegex = "\\d*(?:p\\d*)?"
+ val teamscaleTimestampMatcher = Pattern.compile(teamscaleTimestampRegex).matcher(teamscaleCommitTime)
+ if (teamscaleTimestampMatcher.matches()) {
+ return CommitDescriptor(teamscaleCommitBranch, teamscaleCommitTime)
+ }
+
+ val epochTimestamp: Long
+ try {
+ epochTimestamp = ZonedDateTime.parse(teamscaleCommitTime, dateTimeFormatter).toInstant().toEpochMilli()
+ } catch (e: DateTimeParseException) {
+ throw InvalidGitPropertiesException(
+ ("Cannot parse commit time '" + teamscaleCommitTime + "' in the '" + GIT_PROPERTIES_TEAMSCALE_COMMIT_TIME +
+ "' property. It needs to be in the date formats '" + GIT_PROPERTIES_DEFAULT_MAVEN_DATE_FORMAT +
+ "' or '" + GIT_PROPERTIES_DEFAULT_GRADLE_DATE_FORMAT + "' or match the Teamscale timestamp format '"
+ + teamscaleTimestampRegex + "'." +
+ "\nLocation: Entry '" + entryName + "' in jar file '" + jarFile + "'." +
+ "\nContents of " + GIT_PROPERTIES_FILE_NAME + ":\n" + gitProperties), e
+ )
+ }
+
+ return CommitDescriptor(teamscaleCommitBranch, epochTimestamp)
+ }
+
+ @Throws(InvalidGitPropertiesException::class)
+ private fun getCommitDescriptorFromDefaultGitPropertyValues(
+ gitProperties: Properties,
+ entryName: String?,
+ jarFile: File?,
+ dateTimeFormatter: DateTimeFormatter
+ ): CommitDescriptor? {
+ val gitBranch = gitProperties.getProperty(GIT_PROPERTIES_GIT_BRANCH)
+ val gitTime = gitProperties.getProperty(GIT_PROPERTIES_GIT_COMMIT_TIME)
+ if (!isEmpty(gitBranch) && !isEmpty(gitTime)) {
+ val gitTimestamp: Long
+ try {
+ gitTimestamp = ZonedDateTime.parse(gitTime, dateTimeFormatter).toInstant().toEpochMilli()
+ } catch (e: DateTimeParseException) {
+ throw InvalidGitPropertiesException(
+ "Could not parse the timestamp in property '" + GIT_PROPERTIES_GIT_COMMIT_TIME + "'." +
+ "\nLocation: Entry '" + entryName + "' in jar file '" + jarFile + "'." +
+ "\nContents of " + GIT_PROPERTIES_FILE_NAME + ":\n" + gitProperties, e
+ )
+ }
+ return CommitDescriptor(gitBranch, gitTimestamp)
+ }
+ return null
+ }
+}
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
new file mode 100644
index 000000000..170a60ae1
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitSingleProjectPropertiesLocator.kt
@@ -0,0 +1,104 @@
+package com.teamscale.jacoco.agent.commit_resolution.git_properties
+
+import com.teamscale.jacoco.agent.logging.LoggingUtils.getLogger
+import com.teamscale.jacoco.agent.upload.delay.DelayedUploader
+import com.teamscale.jacoco.agent.util.DaemonThreadFactory
+import java.io.File
+import java.io.IOException
+import java.time.format.DateTimeFormatter
+import java.util.concurrent.Executor
+import java.util.concurrent.Executors
+
+/**
+ * Searches a Jar/War/Ear/... file for a git.properties file in order to enable upload for the commit described therein,
+ * e.g. to Teamscale, via a [DelayedUploader].
+ */
+class GitSingleProjectPropertiesLocator(
+ private val uploader: DelayedUploader,
+ private val dataExtractor: DataExtractor,
+ private val executor: Executor,
+ private val recursiveSearch: Boolean,
+ private val gitPropertiesCommitTimeFormat: DateTimeFormatter?
+) : IGitPropertiesLocator {
+ private val logger = getLogger(this)
+ private var foundData: T? = null
+ private var jarFileWithGitProperties: File? = null
+
+ constructor(
+ uploader: DelayedUploader,
+ dataExtractor: DataExtractor,
+ recursiveSearch: Boolean,
+ gitPropertiesCommitTimeFormat: DateTimeFormatter?
+ ) : this(
+ uploader, dataExtractor, Executors.newSingleThreadExecutor(
+ DaemonThreadFactory(
+ GitSingleProjectPropertiesLocator::class.java,
+ "git.properties Jar scanner thread"
+ )
+ ), recursiveSearch, gitPropertiesCommitTimeFormat
+ )
+
+ /**
+ * Asynchronously searches the given jar file for a git.properties file.
+ */
+ override fun searchFileForGitPropertiesAsync(file: File, isJarFile: Boolean) {
+ executor.execute { searchFile(file, isJarFile) }
+ }
+
+ private fun searchFile(file: File, isJarFile: Boolean) {
+ logger.debug("Searching jar file {} for a single git.properties", file)
+ try {
+ val data = dataExtractor.extractData(file, isJarFile, recursiveSearch, gitPropertiesCommitTimeFormat)
+ if (data.isEmpty()) {
+ logger.debug("No git.properties files found in {}", file.toString())
+ return
+ }
+ if (data.size > 1) {
+ logger.warn(
+ "Multiple git.properties files found in {}", file.toString() +
+ ". Using the first one: " + data.first()
+ )
+ }
+ val dataEntry = data.first()
+
+ if (foundData != null) {
+ if (foundData != dataEntry) {
+ logger.warn(
+ "Found inconsistent git.properties files: {} contained data {} while {} contained {}." +
+ " Please ensure that all git.properties files of your application are consistent." +
+ " Otherwise, you may" +
+ " be uploading to the wrong project/commit which will result in incorrect coverage data" +
+ " displayed in Teamscale. If you cannot fix the inconsistency, you can manually" +
+ " specify a Jar/War/Ear/... file from which to read the correct git.properties" +
+ " file with the agent's teamscale-git-properties-jar parameter.",
+ jarFileWithGitProperties, foundData, file, data
+ )
+ }
+ return
+ }
+
+ logger.debug(
+ "Found git.properties file in {} and found commit descriptor {}", file.toString(),
+ dataEntry
+ )
+ foundData = dataEntry
+ jarFileWithGitProperties = file
+ uploader.setCommitAndTriggerAsynchronousUpload(dataEntry)
+ } catch (e: IOException) {
+ logger.error("Error during asynchronous search for git.properties in {}", file.toString(), e)
+ } catch (e: InvalidGitPropertiesException) {
+ logger.error("Error during asynchronous search for git.properties in {}", file.toString(), e)
+ }
+ }
+
+ /** Functional interface for data extraction from a jar file. */
+ fun interface DataExtractor {
+ /** Extracts data from the JAR. */
+ @Throws(IOException::class, InvalidGitPropertiesException::class)
+ fun extractData(
+ file: File?, isJarFile: Boolean,
+ recursiveSearch: Boolean,
+ gitPropertiesCommitTimeFormat: DateTimeFormatter?
+ ): MutableList
+ }
+}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/IGitPropertiesLocator.java b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/IGitPropertiesLocator.kt
similarity index 61%
rename from agent/src/main/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/IGitPropertiesLocator.java
rename to agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/IGitPropertiesLocator.kt
index 0fc60c7ce..30f724b69 100644
--- a/agent/src/main/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/IGitPropertiesLocator.java
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/IGitPropertiesLocator.kt
@@ -1,13 +1,12 @@
-package com.teamscale.jacoco.agent.commit_resolution.git_properties;
+package com.teamscale.jacoco.agent.commit_resolution.git_properties
-import java.io.File;
-
-/** Interface for the locator classes that search files (e.g., a JAR) for git.properties files containing certain properties. */
-public interface IGitPropertiesLocator {
+import java.io.File
+/** Interface for the locator classes that search files (e.g., a JAR) for git.properties files containing certain properties. */
+interface IGitPropertiesLocator {
/**
* Searches the file for the git.properties file containing certain properties. The boolean flag indicates whether the
* searched file is a JAR file or a plain directory.
*/
- void searchFileForGitPropertiesAsync(File file, boolean isJarFile);
+ fun searchFileForGitPropertiesAsync(file: File, isJarFile: Boolean)
}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/InvalidGitPropertiesException.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/InvalidGitPropertiesException.kt
new file mode 100644
index 000000000..bbfb2e59e
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/InvalidGitPropertiesException.kt
@@ -0,0 +1,9 @@
+package com.teamscale.jacoco.agent.commit_resolution.git_properties
+
+/**
+ * Thrown in case a git.properties file is found but it is malformed.
+ */
+class InvalidGitPropertiesException : Exception {
+ internal constructor(s: String, throwable: Throwable?) : super(s, throwable)
+ constructor(s: String) : super(s)
+}
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
new file mode 100644
index 000000000..b06ac4a18
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/sapnwdi/NwdiMarkerClassLocatingTransformer.kt
@@ -0,0 +1,82 @@
+package com.teamscale.jacoco.agent.commit_resolution.sapnwdi
+
+import com.teamscale.client.CommitDescriptor
+import com.teamscale.client.StringUtils.isEmpty
+import com.teamscale.jacoco.agent.logging.LoggingUtils.getLogger
+import com.teamscale.jacoco.agent.options.sapnwdi.DelayedSapNwdiMultiUploader
+import com.teamscale.jacoco.agent.options.sapnwdi.SapNwdiApplication
+import com.teamscale.report.util.ClasspathWildcardIncludeFilter
+import java.lang.instrument.ClassFileTransformer
+import java.nio.file.Files
+import java.nio.file.Paths
+import java.nio.file.attribute.BasicFileAttributes
+import java.security.ProtectionDomain
+import java.util.function.Function
+import java.util.stream.Collectors
+
+/**
+ * [ClassFileTransformer] that doesn't change the loaded classes but guesses the rough commit timestamp by
+ * inspecting the last modification date of the applications marker class file.
+ */
+class NwdiMarkerClassLocatingTransformer(
+ private val store: DelayedSapNwdiMultiUploader,
+ private val locationIncludeFilter: ClasspathWildcardIncludeFilter,
+ apps: MutableCollection
+) : ClassFileTransformer {
+ private val logger = getLogger(this)
+ private val markerClassesToApplications =
+ apps.associateBy { it.getMarkerClass().replace('.', '/') }
+
+ override fun transform(
+ classLoader: ClassLoader?,
+ className: String,
+ aClass: Class<*>?,
+ protectionDomain: ProtectionDomain?,
+ classFileContent: ByteArray?
+ ): ByteArray? {
+ if (protectionDomain == null) {
+ // happens for e.g. java.lang. We can ignore these classes
+ return null
+ }
+
+ if (className.isEmpty() || !locationIncludeFilter.isIncluded(className)) {
+ // only search in jar files of included classes
+ return null
+ }
+
+ if (!markerClassesToApplications.containsKey(className)) {
+ // only kick off search if the marker class was found.
+ return null
+ }
+
+ try {
+ // unknown when this can happen, we suspect when code is generated at runtime
+ // but there's nothing else we can do here in either case
+ val codeSource = protectionDomain.codeSource ?: return null
+
+ val jarOrClassFolderUrl = codeSource.location
+ logger.debug("Found {} in {}", className, jarOrClassFolderUrl)
+
+ if (jarOrClassFolderUrl.protocol.equals("file", ignoreCase = true)) {
+ val file = Paths.get(jarOrClassFolderUrl.toURI())
+ val attr = Files.readAttributes(file, BasicFileAttributes::class.java)
+ val commitDescriptor = CommitDescriptor(
+ DTR_BRIDGE_DEFAULT_BRANCH, attr.lastModifiedTime().toMillis()
+ )
+ store.setCommitForApplication(commitDescriptor, markerClassesToApplications[className])
+ }
+ } 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 {} trying to determine its last modification timestamp.", className, e
+ )
+ }
+ return null
+ }
+
+ companion object {
+ /** The Design time repository-git-bridge (DTR-bridge) currently only exports a single branch named master. */
+ private const val DTR_BRIDGE_DEFAULT_BRANCH = "master"
+ }
+}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/configuration/AgentOptionReceiveException.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/configuration/AgentOptionReceiveException.kt
new file mode 100644
index 000000000..40c0a3104
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/configuration/AgentOptionReceiveException.kt
@@ -0,0 +1,7 @@
+package com.teamscale.jacoco.agent.configuration
+
+/** Thrown when retrieving the profiler configuration from Teamscale fails. */
+class AgentOptionReceiveException : Exception {
+ constructor(message: String?) : super(message)
+ constructor(message: String?, cause: Throwable?) : super(message, cause)
+}
\ No newline at end of file
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
new file mode 100644
index 000000000..f20312b13
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.kt
@@ -0,0 +1,162 @@
+package com.teamscale.jacoco.agent.configuration
+
+import com.fasterxml.jackson.core.JsonProcessingException
+import com.teamscale.client.ITeamscaleService
+import com.teamscale.client.JsonUtils
+import com.teamscale.client.ProcessInformation
+import com.teamscale.client.ProfilerConfiguration
+import com.teamscale.client.ProfilerInfo
+import com.teamscale.client.ProfilerRegistration
+import com.teamscale.client.TeamscaleServiceGenerator
+import com.teamscale.jacoco.agent.logging.LoggingUtils
+import com.teamscale.jacoco.agent.util.AgentUtils
+import com.teamscale.report.util.ILogger
+import okhttp3.HttpUrl
+import okhttp3.ResponseBody
+import retrofit2.Response
+import java.io.IOException
+import java.time.Duration
+import java.util.concurrent.Executors
+import java.util.concurrent.ThreadFactory
+import java.util.concurrent.TimeUnit
+
+/**
+ * Responsible for holding the configuration retrieved from Teamscale and sending regular heartbeat events to
+ * keep the profiler information in Teamscale up to date.
+ */
+class ConfigurationViaTeamscale(
+ private val teamscaleClient: ITeamscaleService,
+ profilerRegistration: ProfilerRegistration,
+ processInformation: ProcessInformation
+) {
+ /**
+ * The UUID that Teamscale assigned to this instance of the profiler during the registration. This ID needs to be
+ * used when communicating with Teamscale.
+ */
+ @JvmField
+ val profilerId = profilerRegistration.profilerId
+
+ private val profilerInfo = ProfilerInfo(processInformation, profilerRegistration.profilerConfiguration)
+
+ /** Returns the profiler configuration retrieved from Teamscale. */
+ val profilerConfiguration: ProfilerConfiguration?
+ get() = profilerInfo.profilerConfiguration
+
+ /**
+ * Starts a heartbeat thread and registers a shutdown hook.
+ *
+ *
+ * This spawns a new thread every minute which sends a heartbeat to Teamscale. It also registers a shutdown hook
+ * that unregisters the profiler from Teamscale.
+ */
+ fun startHeartbeatThreadAndRegisterShutdownHook() {
+ val executor = Executors.newSingleThreadScheduledExecutor { runnable ->
+ val thread = Thread(runnable)
+ thread.setDaemon(true)
+ thread
+ }
+
+ executor.scheduleAtFixedRate({ sendHeartbeat() }, 1, 1, TimeUnit.MINUTES)
+
+ Runtime.getRuntime().addShutdownHook(Thread {
+ executor.shutdownNow()
+ unregisterProfiler()
+ })
+ }
+
+ private fun sendHeartbeat() {
+ 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()}")
+ }
+ } catch (e: IOException) {
+ LoggingUtils.getLogger(this).error("Failed to send heartbeat to Teamscale!", e)
+ }
+ }
+
+ /** Unregisters the profiler in Teamscale (marks it as shut down). */
+ fun unregisterProfiler() {
+ try {
+ var response = teamscaleClient.unregisterProfiler(profilerId!!).execute()
+ if (response.code() == 405) {
+ response = teamscaleClient.unregisterProfilerLegacy(profilerId).execute()
+ }
+ if (!response.isSuccessful) {
+ LoggingUtils.getLogger(this)
+ .error("Failed to unregister profiler. Teamscale responded with: ${response.errorBody()?.string()}")
+ }
+ } catch (e: IOException) {
+ LoggingUtils.getLogger(this).error("Failed to unregister profiler!", e)
+ }
+ }
+
+ companion object {
+ /**
+ * Two minute timeout. This is quite high to account for an eventual high load on the Teamscale server. This is a
+ * tradeoff between fast application startup and potentially missing test coverage.
+ */
+ private val LONG_TIMEOUT: Duration = Duration.ofMinutes(2)
+
+ /**
+ * Tries to retrieve the profiler configuration from Teamscale. In case retrieval fails the method throws a
+ * [AgentOptionReceiveException].
+ */
+ @JvmStatic
+ @Throws(AgentOptionReceiveException::class)
+ fun retrieve(
+ logger: ILogger,
+ configurationId: String?,
+ url: HttpUrl,
+ userName: String,
+ userAccessToken: String
+ ): ConfigurationViaTeamscale {
+ val teamscaleClient = TeamscaleServiceGenerator
+ .createService(url, userName, userAccessToken, AgentUtils.USER_AGENT, LONG_TIMEOUT, LONG_TIMEOUT)
+ try {
+ val processInformation = ProcessInformationRetriever(logger).processInformation
+ val response = teamscaleClient.registerProfiler(
+ configurationId,
+ processInformation
+ ).execute()
+ if (!response.isSuccessful) {
+ throw AgentOptionReceiveException(
+ "Failed to retrieve profiler configuration from Teamscale due to failed request. Http status: ${response.code()} Body: ${response.errorBody()?.string()}"
+ )
+ }
+
+ 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
+ )
+ }
+ }
+
+ @Throws(AgentOptionReceiveException::class, IOException::class)
+ private fun parseProfilerRegistration(
+ body: ResponseBody,
+ response: Response,
+ teamscaleClient: ITeamscaleService,
+ processInformation: ProcessInformation
+ ): ConfigurationViaTeamscale {
+ // We may only call this once
+ val bodyString = body.string()
+ try {
+ val registration = JsonUtils.deserialize(bodyString)
+ return ConfigurationViaTeamscale(teamscaleClient, registration, processInformation)
+ } catch (e: JsonProcessingException) {
+ throw AgentOptionReceiveException(
+ "Failed to retrieve profiler configuration from Teamscale due to invalid JSON. HTTP code: " + response.code() + " Response: " + bodyString,
+ e
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 000000000..e4692acdd
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/configuration/ProcessInformationRetriever.kt
@@ -0,0 +1,53 @@
+package com.teamscale.jacoco.agent.configuration
+
+import com.teamscale.client.ProcessInformation
+import com.teamscale.report.util.ILogger
+import java.lang.management.ManagementFactory
+import java.net.InetAddress
+import java.net.UnknownHostException
+
+/**
+ * Is responsible for retrieving process information such as the host name and process ID.
+ */
+class ProcessInformationRetriever(private val logger: ILogger) {
+ /**
+ * Retrieves the process information, including the host name and process ID.
+ */
+ val processInformation: ProcessInformation
+ get() = ProcessInformation(hostName, pID, System.currentTimeMillis())
+
+ /**
+ * Retrieves the host name of the local machine.
+ */
+ private val hostName: String
+ get() {
+ try {
+ return InetAddress.getLocalHost().hostName
+ } catch (e: UnknownHostException) {
+ logger.error("Failed to determine hostname!", e)
+ return ""
+ }
+ }
+
+ /**
+ * Returns a string that *probably* contains the PID.
+ *
+ * On Java 9 there is an API to get the PID. But since we support Java 8, we may fall back to an undocumented API
+ * that at least contains the PID in most JVMs.
+ *
+ * See [This StackOverflow question](https://stackoverflow.com/questions/35842/how-can-a-java-program-get-its-own-process-id)
+ */
+ companion object {
+ val pID: String
+ get() {
+ try {
+ val processHandleClass = Class.forName("java.lang.ProcessHandle")
+ val processHandle = processHandleClass.getMethod("current").invoke(null)
+ val pid = processHandleClass.getMethod("pid").invoke(processHandle) as Long
+ return pid.toString()
+ } catch (_: ReflectiveOperationException) {
+ return ManagementFactory.getRuntimeMXBean().name
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/convert/ConvertCommand.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/convert/ConvertCommand.kt
new file mode 100644
index 000000000..2183de6f5
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/convert/ConvertCommand.kt
@@ -0,0 +1,156 @@
+/*-------------------------------------------------------------------------+
+| |
+| Copyright (c) 2009-2017 CQSE GmbH |
+| |
++-------------------------------------------------------------------------*/
+package com.teamscale.jacoco.agent.convert
+
+import com.beust.jcommander.Parameter
+import com.beust.jcommander.Parameters
+import com.teamscale.client.FileSystemUtils.ensureDirectoryExists
+import com.teamscale.client.StringUtils.isEmpty
+import com.teamscale.jacoco.agent.commandline.ICommand
+import com.teamscale.jacoco.agent.commandline.Validator
+import com.teamscale.jacoco.agent.options.ClasspathUtils
+import com.teamscale.jacoco.agent.options.FilePatternResolver
+import com.teamscale.jacoco.agent.util.Assertions
+import com.teamscale.report.EDuplicateClassFileBehavior
+import com.teamscale.report.util.CommandLineLogger
+import java.io.File
+import java.io.IOException
+
+/**
+ * Encapsulates all command line options for the convert command for parsing with [JCommander].
+ */
+@Parameters(
+ commandNames = ["convert"], commandDescription = "Converts a binary .exec coverage file to XML. " +
+ "Note that the XML report will only contain source file coverage information, but no class coverage."
+)
+class ConvertCommand : ICommand {
+ /** The directories and/or zips that contain all class files being profiled. */
+ @JvmField
+ @Parameter(
+ names = ["--class-dir", "--jar", "-c"], required = true, description = (""
+ + "The directories or zip/ear/jar/war/... files that contain the compiled Java classes being profiled."
+ + " Searches recursively, including inside zips. You may also supply a *.txt file with one path per line.")
+ )
+ var classDirectoriesOrZips = mutableListOf()
+
+ /**
+ * Wildcard include patterns to apply during JaCoCo's traversal of class files.
+ */
+ @Parameter(
+ names = ["--includes"], description = (""
+ + "Wildcard include patterns to apply to all found class file locations during JaCoCo's traversal of class files."
+ + " Note that zip contents are separated from zip files with @ and that you can filter only"
+ + " class files, not intermediate folders/zips. Use with great care as missing class files"
+ + " lead to broken coverage files! Turn on debug logging to see which locations are being filtered."
+ + " Defaults to no filtering. Excludes overrule includes.")
+ )
+ var locationIncludeFilters = mutableListOf()
+
+ /**
+ * Wildcard exclude patterns to apply during JaCoCo's traversal of class files.
+ */
+ @Parameter(
+ names = ["--excludes", "-e"], description = (""
+ + "Wildcard exclude patterns to apply to all found class file locations during JaCoCo's traversal of class files."
+ + " Note that zip contents are separated from zip files with @ and that you can filter only"
+ + " class files, not intermediate folders/zips. Use with great care as missing class files"
+ + " lead to broken coverage files! Turn on debug logging to see which locations are being filtered."
+ + " Defaults to no filtering. Excludes overrule includes.")
+ )
+ var locationExcludeFilters = mutableListOf()
+
+ /** The directory to write the XML traces to. */
+ @JvmField
+ @Parameter(
+ names = ["--in", "-i"], required = true, description = ("" + "The binary .exec file(s), test details and " +
+ "test executions to read. Can be a single file or a directory that is recursively scanned for relevant files.")
+ )
+ var inputFiles = mutableListOf()
+
+ /** The directory to write the XML traces to. */
+ @JvmField
+ @Parameter(
+ names = ["--out", "-o"], required = true, description = (""
+ + "The file to write the generated XML report to.")
+ )
+ var outputFile = ""
+
+ /** Whether to ignore duplicate, non-identical class files. */
+ @Parameter(
+ names = ["--duplicates", "-d"], arity = 1, description = (""
+ + "Whether to ignore duplicate, non-identical class files."
+ + " This is discouraged and may result in incorrect coverage files. Defaults to WARN. " +
+ "Options are FAIL, WARN and IGNORE.")
+ )
+ var duplicateClassFileBehavior = EDuplicateClassFileBehavior.WARN
+
+ /** Whether to ignore uncovered class files. */
+ @Parameter(
+ names = ["--ignore-uncovered-classes"], required = false, arity = 1, description = (""
+ + "Whether to ignore uncovered classes."
+ + " These classes will not be part of the XML report at all, making it considerably smaller in some cases. Defaults to false.")
+ )
+ var shouldIgnoreUncoveredClasses = false
+
+ /** Whether testwise coverage or jacoco coverage should be generated. */
+ @Parameter(
+ names = ["--testwise-coverage", "-t"], required = false, arity = 0, description = "Whether testwise " +
+ "coverage or jacoco coverage should be generated."
+ )
+ var shouldGenerateTestwiseCoverage = false
+
+ /** After how many tests testwise coverage should be split into multiple reports. */
+ @Parameter(
+ names = ["--split-after", "-s"], required = false, arity = 1, description = "After how many tests " +
+ "testwise coverage should be split into multiple reports (Default is 5000)."
+ )
+ val splitAfter = 5000
+
+ @Throws(IOException::class)
+ fun getClassDirectoriesOrZips(): List = ClasspathUtils
+ .resolveClasspathTextFiles(
+ "class-dir", FilePatternResolver(CommandLineLogger()),
+ classDirectoriesOrZips
+ )
+
+ fun getInputFiles() = inputFiles.map { File(it) }
+ fun getOutputFile() = File(outputFile)
+
+ /** Makes sure the arguments are valid. */
+ override fun validate() = Validator().apply {
+ val classDirectoriesOrZips = mutableListOf()
+ ensure { classDirectoriesOrZips.addAll(getClassDirectoriesOrZips()) }
+ isFalse(
+ classDirectoriesOrZips.isEmpty(),
+ "You must specify at least one directory or zip that contains class files"
+ )
+ classDirectoriesOrZips.forEach { path ->
+ isTrue(path.exists(), "Path '$path' does not exist")
+ isTrue(path.canRead(), "Path '$path' is not readable")
+ }
+ getInputFiles().forEach { inputFile ->
+ isTrue(inputFile.exists() && inputFile.canRead(), "Cannot read the input file $inputFile")
+ }
+ ensure {
+ Assertions.isFalse(isEmpty(outputFile), "You must specify an output file")
+ val outputDir = getOutputFile().getAbsoluteFile().getParentFile()
+ ensureDirectoryExists(outputDir)
+ Assertions.isTrue(outputDir.canWrite(), "Path '$outputDir' is not writable")
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Throws(Exception::class)
+ override fun run() {
+ Converter(this).apply {
+ if (shouldGenerateTestwiseCoverage) {
+ runTestwiseCoverageReportGeneration()
+ } else {
+ runJaCoCoReportGeneration()
+ }
+ }
+ }
+}
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
new file mode 100644
index 000000000..0506d8acf
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/convert/Converter.kt
@@ -0,0 +1,99 @@
+package com.teamscale.jacoco.agent.convert
+
+import com.teamscale.client.TestDetails
+import com.teamscale.jacoco.agent.benchmark
+import com.teamscale.jacoco.agent.logging.LoggingUtils
+import com.teamscale.jacoco.agent.options.AgentOptionParseException
+import com.teamscale.jacoco.agent.util.Benchmark
+import com.teamscale.report.ReportUtils
+import com.teamscale.report.ReportUtils.listFiles
+import com.teamscale.report.jacoco.EmptyReportException
+import com.teamscale.report.jacoco.JaCoCoXmlReportGenerator
+import com.teamscale.report.testwise.ETestArtifactFormat
+import com.teamscale.report.testwise.TestwiseCoverageReportWriter
+import com.teamscale.report.testwise.jacoco.JaCoCoTestwiseReportGenerator
+import com.teamscale.report.testwise.model.TestExecution
+import com.teamscale.report.testwise.model.factory.TestInfoFactory
+import com.teamscale.report.util.ClasspathWildcardIncludeFilter
+import com.teamscale.report.util.CommandLineLogger
+import com.teamscale.report.util.ILogger
+import java.io.File
+import java.io.IOException
+import java.lang.String
+import java.nio.file.Paths
+import kotlin.Array
+import kotlin.Throws
+import kotlin.use
+
+/** Converts one .exec binary coverage file to XML. */
+class Converter
+/** Constructor. */(
+ /** The command line arguments. */
+ private val arguments: ConvertCommand
+) {
+ /** Converts one .exec binary coverage file to XML. */
+ @Throws(IOException::class)
+ fun runJaCoCoReportGeneration() {
+ val logger = LoggingUtils.getLogger(this)
+ val generator = JaCoCoXmlReportGenerator(
+ arguments.getClassDirectoriesOrZips(),
+ wildcardIncludeExcludeFilter,
+ arguments.duplicateClassFileBehavior,
+ arguments.shouldIgnoreUncoveredClasses,
+ LoggingUtils.wrap(logger)
+ )
+
+ val jacocoExecutionDataList = listFiles(ETestArtifactFormat.JACOCO, arguments.getInputFiles())
+ try {
+ benchmark("Generating the XML report") {
+ generator.convertExecFilesToReport(jacocoExecutionDataList, Paths.get(arguments.outputFile).toFile())
+ }
+ } catch (e: EmptyReportException) {
+ logger.warn("Converted report was empty.", e)
+ }
+ }
+
+ /** Converts one .exec binary coverage file, test details and test execution files to JSON testwise coverage. */
+ @Throws(IOException::class, AgentOptionParseException::class)
+ fun runTestwiseCoverageReportGeneration() {
+ val testDetails = ReportUtils.readObjects(
+ ETestArtifactFormat.TEST_LIST,
+ Array::class.java,
+ arguments.getInputFiles()
+ )
+ val testExecutions = ReportUtils.readObjects(
+ ETestArtifactFormat.TEST_EXECUTION,
+ Array::class.java,
+ arguments.getInputFiles()
+ )
+
+ val jacocoExecutionDataList = listFiles(ETestArtifactFormat.JACOCO, arguments.getInputFiles())
+ val logger = CommandLineLogger()
+
+ val generator = JaCoCoTestwiseReportGenerator(
+ arguments.getClassDirectoriesOrZips(),
+ this.wildcardIncludeExcludeFilter,
+ arguments.duplicateClassFileBehavior,
+ logger
+ )
+
+ benchmark("Generating the testwise coverage report") {
+ logger.info("Writing report with ${testDetails.size} Details/${testExecutions.size} Results")
+ TestwiseCoverageReportWriter(
+ TestInfoFactory(testDetails, testExecutions),
+ arguments.getOutputFile(),
+ arguments.splitAfter, null
+ ).use { coverageWriter ->
+ jacocoExecutionDataList.forEach { executionDataFile ->
+ generator.convertAndConsume(executionDataFile, coverageWriter)
+ }
+ }
+ }
+ }
+
+ private val wildcardIncludeExcludeFilter: ClasspathWildcardIncludeFilter
+ get() = ClasspathWildcardIncludeFilter(
+ String.join(":", arguments.locationIncludeFilters),
+ String.join(":", arguments.locationExcludeFilters)
+ )
+}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/logging/DebugLogDirectoryPropertyDefiner.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/logging/DebugLogDirectoryPropertyDefiner.kt
new file mode 100644
index 000000000..876e989f2
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/logging/DebugLogDirectoryPropertyDefiner.kt
@@ -0,0 +1,14 @@
+package com.teamscale.jacoco.agent.logging
+
+import java.nio.file.Path
+
+/** Defines a property that contains the path to which log files should be written. */
+class DebugLogDirectoryPropertyDefiner : LogDirectoryPropertyDefiner() {
+ override fun getPropertyValue() =
+ filePath?.resolve("logs")?.toAbsolutePath()?.toString() ?: super.getPropertyValue()
+
+ companion object {
+ /** File path for debug logging. */ /* package */
+ var filePath: Path? = null
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/logging/LogDirectoryPropertyDefiner.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/logging/LogDirectoryPropertyDefiner.kt
new file mode 100644
index 000000000..c587a45f0
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/logging/LogDirectoryPropertyDefiner.kt
@@ -0,0 +1,10 @@
+package com.teamscale.jacoco.agent.logging
+
+import ch.qos.logback.core.PropertyDefinerBase
+import com.teamscale.jacoco.agent.util.AgentUtils
+
+/** Defines a property that contains the default path to which log files should be written. */
+open class LogDirectoryPropertyDefiner : PropertyDefinerBase() {
+ override fun getPropertyValue() =
+ AgentUtils.mainTempDirectory.resolve("logs").toAbsolutePath().toString()
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.kt
new file mode 100644
index 000000000..f646f9848
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.kt
@@ -0,0 +1,183 @@
+package com.teamscale.jacoco.agent.logging
+
+import ch.qos.logback.classic.Logger
+import ch.qos.logback.classic.LoggerContext
+import ch.qos.logback.classic.spi.ILoggingEvent
+import ch.qos.logback.core.AppenderBase
+import ch.qos.logback.core.status.ErrorStatus
+import com.teamscale.client.ITeamscaleService
+import com.teamscale.client.ProfilerLogEntry
+import com.teamscale.jacoco.agent.options.AgentOptions
+import java.net.ConnectException
+import java.time.Duration
+import java.util.Collections
+import java.util.IdentityHashMap
+import java.util.LinkedHashSet
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.Executors
+import java.util.concurrent.ScheduledExecutorService
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.function.BiConsumer
+
+/**
+ * Custom log appender that sends logs to Teamscale; it buffers log that were not sent due to connection issues and
+ * sends them later.
+ */
+class LogToTeamscaleAppender : AppenderBase() {
+ /** The unique ID of the profiler */
+ private var profilerId: String? = null
+
+ /**
+ * Buffer for unsent logs. We use a set here to allow for removing entries fast after sending them to Teamscale was
+ * successful.
+ */
+ private val logBuffer = LinkedHashSet()
+
+ /** Scheduler for sending logs after the configured time interval */
+ private val scheduler: ScheduledExecutorService = Executors.newScheduledThreadPool(1) { r ->
+ // Make the thread a daemon so that it does not prevent the JVM from terminating.
+ val t = Executors.defaultThreadFactory().newThread(r)
+ t.setDaemon(true)
+ t
+ }
+
+ /** Active log flushing threads */
+ private val activeLogFlushes: MutableSet> =
+ Collections.newSetFromMap(IdentityHashMap())
+
+ /** Is there a flush going on right now? */
+ private val isFlusing = AtomicBoolean(false)
+
+ override fun start() {
+ super.start()
+ scheduler.scheduleAtFixedRate({
+ synchronized(activeLogFlushes) {
+ activeLogFlushes.removeIf { it.isDone }
+ if (activeLogFlushes.isEmpty()) flush()
+ }
+ }, FLUSH_INTERVAL.toMillis(), FLUSH_INTERVAL.toMillis(), TimeUnit.MILLISECONDS)
+ }
+
+ override fun append(eventObject: ILoggingEvent) {
+ synchronized(logBuffer) {
+ logBuffer.add(formatLog(eventObject))
+ if (logBuffer.size >= BATCH_SIZE) flush()
+ }
+ }
+
+ private fun formatLog(eventObject: ILoggingEvent): ProfilerLogEntry {
+ val trace = LoggingUtils.getStackTraceFromEvent(eventObject)
+ val timestamp = eventObject.timeStamp
+ val message = eventObject.formattedMessage
+ val severity = eventObject.level.toString()
+ return ProfilerLogEntry(timestamp, message, trace, severity)
+ }
+
+ private fun flush() {
+ sendLogs()
+ }
+
+ /** Send logs in a separate thread */
+ private fun sendLogs() {
+ synchronized(activeLogFlushes) {
+ activeLogFlushes.add(CompletableFuture.runAsync {
+ if (isFlusing.compareAndSet(false, true)) {
+ try {
+ val client = teamscaleClient ?: return@runAsync // There might be no connection configured.
+
+ val logsToSend: MutableList
+ synchronized(logBuffer) {
+ logsToSend = logBuffer.toMutableList()
+ }
+
+ val call = client.postProfilerLog(profilerId!!, logsToSend)
+ val response = call.execute()
+ check(response.isSuccessful) { "Failed to send log: HTTP error code : ${response.code()}" }
+
+ synchronized(logBuffer) {
+ // Removing the logs that have been sent after the fact.
+ // This handles problems with lost network connections.
+ logBuffer.removeAll(logsToSend.toSet())
+ }
+ } catch (e: Exception) {
+ // We do not report on exceptions here.
+ if (e !is ConnectException) {
+ addStatus(ErrorStatus("Sending logs to Teamscale failed: ${e.message}", this, e))
+ }
+ } finally {
+ isFlusing.set(false)
+ }
+ }
+ }.whenComplete(BiConsumer { _, _ ->
+ synchronized(activeLogFlushes) {
+ activeLogFlushes.removeIf { it.isDone }
+ }
+ }))
+ }
+ }
+
+ override fun stop() {
+ // Already flush here once to make sure that we do not miss too much.
+ flush()
+
+ scheduler.shutdown()
+ try {
+ if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
+ scheduler.shutdownNow()
+ }
+ } catch (_: InterruptedException) {
+ scheduler.shutdownNow()
+ }
+
+ // A final flush after the scheduler has been shut down.
+ flush()
+
+ // Block until all flushes are done
+ CompletableFuture.allOf(*activeLogFlushes.toTypedArray()).join()
+
+ super.stop()
+ }
+
+ fun setTeamscaleClient(teamscaleClient: ITeamscaleService?) {
+ Companion.teamscaleClient = teamscaleClient
+ }
+
+ fun setProfilerId(profilerId: String) {
+ this.profilerId = profilerId
+ }
+
+ companion object {
+ /** Flush the logs after N elements are in the queue */
+ private const val BATCH_SIZE = 50
+
+ /** Flush the logs in the given time interval */
+ private val FLUSH_INTERVAL: Duration = Duration.ofSeconds(3)
+
+ /** The service client for sending logs to Teamscale */
+ private var teamscaleClient: ITeamscaleService? = null
+
+ /**
+ * Add the [LogToTeamscaleAppender] to the logging configuration and
+ * enable/start it.
+ */
+ fun addTeamscaleAppenderTo(context: LoggerContext, agentOptions: AgentOptions): Boolean {
+ val client = agentOptions.createTeamscaleClient(false)
+ if (client == null || agentOptions.configurationViaTeamscale == null) {
+ return false
+ }
+
+ context.getLogger(Logger.ROOT_LOGGER_NAME).apply {
+ val logToTeamscaleAppender = LogToTeamscaleAppender().apply {
+ setContext(context)
+ setProfilerId(agentOptions.configurationViaTeamscale.profilerId!!)
+ setTeamscaleClient(client.service)
+ start()
+ }
+ addAppender(logToTeamscaleAppender)
+ }
+
+ return true
+ }
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/logging/LoggingUtils.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/logging/LoggingUtils.kt
new file mode 100644
index 000000000..0e0a244a9
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/logging/LoggingUtils.kt
@@ -0,0 +1,124 @@
+package com.teamscale.jacoco.agent.logging
+
+import ch.qos.logback.classic.LoggerContext
+import ch.qos.logback.classic.joran.JoranConfigurator
+import ch.qos.logback.classic.spi.ILoggingEvent
+import ch.qos.logback.classic.spi.ThrowableProxy
+import ch.qos.logback.classic.spi.ThrowableProxyUtil
+import ch.qos.logback.core.joran.spi.JoranException
+import ch.qos.logback.core.util.StatusPrinter
+import com.teamscale.jacoco.agent.Agent
+import com.teamscale.jacoco.agent.util.NullOutputStream
+import com.teamscale.report.util.ILogger
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import java.io.FileInputStream
+import java.io.IOException
+import java.io.InputStream
+import java.io.PrintStream
+import java.lang.AutoCloseable
+import java.nio.file.Path
+
+/**
+ * Helps initialize the logging framework properly.
+ */
+object LoggingUtils {
+ /** Returns a logger for the given object's class. */
+ @JvmStatic
+ fun getLogger(obj: Any): Logger = LoggerFactory.getLogger(obj.javaClass)
+
+ /** Returns a logger for the given class. */
+ @JvmStatic
+ fun getLogger(obj: Class<*>): Logger = LoggerFactory.getLogger(obj)
+
+ /** Initializes the logging to the default configured in the Jar. */
+ fun initializeDefaultLogging(): LoggingResources {
+ val stream = Agent::class.java.getResourceAsStream("logback-default.xml")
+ reconfigureLoggerContext(stream)
+ return LoggingResources()
+ }
+
+ /**
+ * Returns the logger context.
+ */
+ val loggerContext: LoggerContext
+ get() = LoggerFactory.getILoggerFactory() as LoggerContext
+
+ /**
+ * Extracts the stack trace from an ILoggingEvent using ThrowableProxyUtil.
+ *
+ * @param event the logging event containing the exception
+ * @return the stack trace as a String, or null if no exception is associated
+ */
+ fun getStackTraceFromEvent(event: ILoggingEvent) =
+ event.throwableProxy?.let { ThrowableProxyUtil.asString(it) }
+
+ /**
+ * Converts a Throwable to its stack trace as a String.
+ *
+ * @param throwable the throwable to convert
+ * @return the stack trace as a String
+ */
+ @JvmStatic
+ fun getStackTraceAsString(throwable: Throwable?) =
+ throwable?.let { ThrowableProxyUtil.asString(ThrowableProxy(it)) }
+
+ /**
+ * Reconfigures the logger context to use the configuration XML from the given input stream. Cf. [https://logback.qos.ch/manual/configuration.html](https://logback.qos.ch/manual/configuration.html)
+ */
+ private fun reconfigureLoggerContext(stream: InputStream?) {
+ StatusPrinter.setPrintStream(PrintStream(NullOutputStream()))
+ try {
+ val configurator = JoranConfigurator()
+ configurator.setContext(loggerContext)
+ loggerContext.reset()
+ configurator.doConfigure(stream)
+ } catch (_: JoranException) {
+ // StatusPrinter will handle this
+ }
+ StatusPrinter.printInCaseOfErrorsOrWarnings(loggerContext)
+ }
+
+ /**
+ * Initializes the logging from the given file. If that is `null`, uses [ ][.initializeDefaultLogging] instead.
+ */
+ @Throws(IOException::class)
+ fun initializeLogging(loggingConfigFile: Path?): LoggingResources {
+ if (loggingConfigFile == null) {
+ return initializeDefaultLogging()
+ }
+
+ reconfigureLoggerContext(FileInputStream(loggingConfigFile.toFile()))
+ return LoggingResources()
+ }
+
+ /** Initializes debug logging. */
+ fun initializeDebugLogging(logDirectory: Path?): LoggingResources {
+ if (logDirectory != null) {
+ DebugLogDirectoryPropertyDefiner.filePath = logDirectory
+ }
+ val stream = Agent::class.java.getResourceAsStream("logback-default-debugging.xml")
+ reconfigureLoggerContext(stream)
+ return LoggingResources()
+ }
+
+ /** Wraps the given slf4j logger into an [com.teamscale.report.util.ILogger]. */
+ @JvmStatic
+ fun wrap(logger: Logger): ILogger {
+ return object : ILogger {
+ override fun debug(message: String) = logger.debug(message)
+ override fun info(message: String) = logger.info(message)
+ override fun warn(message: String) = logger.warn(message)
+ override fun warn(message: String, throwable: Throwable?) = logger.warn(message, throwable)
+ override fun error(throwable: Throwable) = logger.error(throwable.message, throwable)
+ override fun error(message: String, throwable: Throwable?) = logger.error(message, throwable)
+ }
+ }
+
+ /** Class to use with try-with-resources to close the logging framework's resources. */
+ class LoggingResources : AutoCloseable {
+ override fun close() {
+ loggerContext.stop()
+ }
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/ProjectAndCommit.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/ProjectAndCommit.kt
new file mode 100644
index 000000000..260eb3a7c
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/options/ProjectAndCommit.kt
@@ -0,0 +1,10 @@
+package com.teamscale.jacoco.agent.options
+
+import com.teamscale.jacoco.agent.commit_resolution.git_properties.CommitInfo
+import java.util.*
+
+/** Class encapsulating the Teamscale project and git commitInfo an upload should be performed to. */
+data class ProjectAndCommit(
+ @JvmField val project: String?,
+ @JvmField val commitInfo: CommitInfo?
+)
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
new file mode 100644
index 000000000..34c0cc88f
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/DelayedMultiUploaderBase.kt
@@ -0,0 +1,39 @@
+package com.teamscale.jacoco.agent.upload
+
+import com.teamscale.jacoco.agent.logging.LoggingUtils.getLogger
+import com.teamscale.report.jacoco.CoverageFile
+import org.slf4j.Logger
+import java.util.function.Consumer
+import java.util.stream.Collectors
+
+/**
+ * Base class for wrapper uploaders that allow uploading the same coverage to
+ * multiple locations.
+ */
+abstract class DelayedMultiUploaderBase : IUploader {
+ @JvmField
+ protected val logger: Logger = getLogger(this)
+
+ @Synchronized
+ override fun upload(coverageFile: CoverageFile) {
+ 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")
+ } else {
+ wrappedUploaders.forEach { wrappedUploader ->
+ wrappedUploader.upload(coverageFile)
+ }
+ }
+ }
+
+ override fun describe(): String {
+ if (!wrappedUploaders.isEmpty()) {
+ return wrappedUploaders.joinToString { it.describe() }
+ }
+ return "Temporary stand-in until commit is resolved"
+ }
+
+ /** Returns the actual uploaders that this multiuploader wraps. */
+ protected abstract val wrappedUploaders: MutableCollection
+}
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
new file mode 100644
index 000000000..02129b018
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/HttpZipUploaderBase.kt
@@ -0,0 +1,147 @@
+package com.teamscale.jacoco.agent.upload
+
+import com.teamscale.client.FileSystemUtils.readFileBinary
+import com.teamscale.client.HttpUtils.createRetrofit
+import com.teamscale.jacoco.agent.benchmark
+import com.teamscale.jacoco.agent.logging.LoggingUtils.getLogger
+import com.teamscale.jacoco.agent.util.Benchmark
+import com.teamscale.report.jacoco.CoverageFile
+import okhttp3.HttpUrl
+import okhttp3.OkHttpClient
+import okhttp3.ResponseBody
+import org.slf4j.Logger
+import retrofit2.Response
+import retrofit2.Retrofit
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import java.nio.file.Files
+import java.nio.file.Path
+import java.util.function.Consumer
+import java.util.zip.ZipEntry
+import java.util.zip.ZipOutputStream
+
+/** Base class for uploading the coverage zip to a provided url */
+abstract class HttpZipUploaderBase
+/** Constructor. */(
+ /** The URL to upload to. */
+ @JvmField
+ protected var uploadUrl: HttpUrl,
+ /** Additional files to include in the uploaded zip. */
+ protected val additionalMetaDataFiles: MutableList,
+ /** The API class. */
+ private val apiClass: Class
+) : IUploader {
+ /** The logger. */
+ @JvmField
+ protected val logger: Logger = getLogger(this)
+
+ /** The API which performs the upload */
+ protected val api: T by lazy {
+ val retrofit = createRetrofit(
+ { baseUrl(uploadUrl) },
+ { configureOkHttp(this) }
+ )
+ retrofit.create(apiClass)
+ }
+
+ /** Template method to configure the OkHttp Client. */
+ protected open fun configureOkHttp(builder: OkHttpClient.Builder) {
+ }
+
+ /** Uploads the coverage zip to the server */
+ @Throws(IOException::class, UploaderException::class)
+ protected abstract fun uploadCoverageZip(coverageFile: File): Response
+
+ override fun upload(coverageFile: CoverageFile) {
+ try {
+ benchmark("Uploading report via HTTP") {
+ if (tryUpload(coverageFile)) {
+ coverageFile.delete()
+ } else {
+ logger.warn(
+ ("Failed to upload coverage to Teamscale. "
+ + "Won't delete local file {} so that the upload can automatically be retried upon profiler restart. "
+ + "Upload can also be retried manually."), coverageFile
+ )
+ (this as? IUploadRetry)?.markFileForUploadRetry(coverageFile)
+ }
+ }
+ } catch (_: IOException) {
+ logger.warn("Could not delete file {} after upload", coverageFile)
+ }
+ }
+
+ /** Performs the upload and returns `true` if successful. */
+ protected fun tryUpload(coverageFile: CoverageFile): Boolean {
+ logger.debug("Uploading coverage to {}", uploadUrl)
+
+ val zipFile: File
+ try {
+ zipFile = createZipFile(coverageFile)
+ } catch (e: IOException) {
+ logger.error("Failed to compile coverage zip file for upload to {}", uploadUrl, e)
+ return false
+ }
+
+ try {
+ val response = uploadCoverageZip(zipFile)
+ if (response.isSuccessful) {
+ return true
+ }
+
+ var errorBody = ""
+ if (response.errorBody() != null) {
+ errorBody = response.errorBody()!!.string()
+ }
+
+ logger.error(
+ "Failed to upload coverage to {}. Request failed with error code {}. Error:\n{}", uploadUrl,
+ response.code(), errorBody
+ )
+ return false
+ } catch (e: IOException) {
+ logger.error("Failed to upload coverage to {}. Probably a network problem", uploadUrl, e)
+ return false
+ } catch (e: UploaderException) {
+ logger.error("Failed to upload coverage to {}. The configuration is probably incorrect", uploadUrl, e)
+ return false
+ } finally {
+ zipFile.delete()
+ }
+ }
+
+ /**
+ * Creates the zip file in the system temp directory to upload which includes the given coverage XML and all
+ * [.additionalMetaDataFiles]. The file is marked to be deleted on exit.
+ */
+ @Throws(IOException::class)
+ private fun createZipFile(coverageFile: CoverageFile): File {
+ val zipFile = Files.createTempFile(coverageFile.nameWithoutExtension, ".zip").toFile()
+ zipFile.deleteOnExit()
+ FileOutputStream(zipFile).use { fileOutputStream ->
+ ZipOutputStream(fileOutputStream).use { zipOutputStream ->
+ fillZipFile(zipOutputStream, coverageFile)
+ return zipFile
+ }
+ }
+ }
+
+ /**
+ * Fills the upload zip file with the given coverage XML and all [.additionalMetaDataFiles].
+ */
+ @Throws(IOException::class)
+ private fun fillZipFile(zipOutputStream: ZipOutputStream, coverageFile: CoverageFile) {
+ zipOutputStream.putNextEntry(ZipEntry(getZipEntryCoverageFileName(coverageFile)))
+ coverageFile.copyStream(zipOutputStream)
+
+ for (additionalFile in additionalMetaDataFiles) {
+ zipOutputStream.putNextEntry(ZipEntry(additionalFile.fileName.toString()))
+ zipOutputStream.write(readFileBinary(additionalFile.toFile()))
+ }
+ }
+
+ protected open fun getZipEntryCoverageFileName(coverageFile: CoverageFile): String {
+ return "coverage.xml"
+ }
+}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/upload/IUploadRetry.java b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/IUploadRetry.kt
similarity index 53%
rename from agent/src/main/java/com/teamscale/jacoco/agent/upload/IUploadRetry.java
rename to agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/IUploadRetry.kt
index eaf9fa9ed..d3ff3393f 100644
--- a/agent/src/main/java/com/teamscale/jacoco/agent/upload/IUploadRetry.java
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/IUploadRetry.kt
@@ -1,23 +1,21 @@
-package com.teamscale.jacoco.agent.upload;
+package com.teamscale.jacoco.agent.upload
-import java.util.Properties;
-
-import com.teamscale.report.jacoco.CoverageFile;
+import com.teamscale.report.jacoco.CoverageFile
+import java.util.*
/**
* Interface for all the uploaders that support an automatic upload retry
* mechanism.
*/
-public interface IUploadRetry {
-
+interface IUploadRetry {
/**
* Marks coverage files of unsuccessful coverage uploads so that they can be
* reuploaded at next agent start.
*/
- void markFileForUploadRetry(CoverageFile coverageFile);
+ fun markFileForUploadRetry(coverageFile: CoverageFile)
/**
* Retries previously unsuccessful coverage uploads with the given properties.
*/
- void reupload(CoverageFile coverageFile, Properties properties);
+ fun reupload(coverageFile: CoverageFile, properties: Properties)
}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/IUploader.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/IUploader.kt
new file mode 100644
index 000000000..bc85d0004
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/IUploader.kt
@@ -0,0 +1,16 @@
+package com.teamscale.jacoco.agent.upload
+
+import com.teamscale.report.jacoco.CoverageFile
+
+/** Uploads coverage reports. */
+interface IUploader {
+ /**
+ * Uploads the given coverage file. If the upload was successful, the coverage
+ * file on disk will be deleted. Otherwise the file is left on disk and a
+ * warning is logged.
+ */
+ fun upload(coverageFile: CoverageFile)
+
+ /** Human-readable description of the uploader. */
+ fun describe(): String
+}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/LocalDiskUploader.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/LocalDiskUploader.kt
new file mode 100644
index 000000000..c4ebe0681
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/LocalDiskUploader.kt
@@ -0,0 +1,16 @@
+package com.teamscale.jacoco.agent.upload
+
+import com.teamscale.report.jacoco.CoverageFile
+
+/**
+ * Dummy uploader which keeps the coverage file written by the agent on disk,
+ * but does not actually perform uploads.
+ */
+class LocalDiskUploader : IUploader {
+ override fun upload(coverageFile: CoverageFile) {
+ // Don't delete the file here. We want to store the file permanently on disk in
+ // case no uploader is configured.
+ }
+
+ override fun describe() = "configured output directory on the local disk"
+}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/UploaderException.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/UploaderException.kt
new file mode 100644
index 000000000..bd50cca8f
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/UploaderException.kt
@@ -0,0 +1,32 @@
+package com.teamscale.jacoco.agent.upload
+
+import okhttp3.ResponseBody
+import retrofit2.Response
+import java.io.IOException
+
+/**
+ * Exception thrown from an uploader. Either during the upload or in the validation process.
+ */
+class UploaderException : Exception {
+ /** Constructor */
+ constructor(message: String, e: Exception) : super(message, e)
+
+ /** Constructor */
+ constructor(message: String) : super(message)
+
+ /** Constructor */
+ constructor(message: String, response: Response) : super(createResponseMessage(message, response))
+
+ companion object {
+ private fun createResponseMessage(message: String, response: Response): String {
+ try {
+ val errorBodyMessage = response.errorBody()!!.string()
+ return "$message (${response.code()}): \n$errorBodyMessage"
+ } catch (_: IOException) {
+ return message
+ } catch (_: NullPointerException) {
+ return message
+ }
+ }
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 000000000..9d42f4e6c
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/artifactory/ArtifactoryConfig.kt
@@ -0,0 +1,206 @@
+package com.teamscale.jacoco.agent.upload.artifactory
+
+import com.teamscale.client.StringUtils.stripSuffix
+import com.teamscale.jacoco.agent.commit_resolution.git_properties.CommitInfo
+import com.teamscale.jacoco.agent.commit_resolution.git_properties.GitPropertiesLocatorUtils
+import com.teamscale.jacoco.agent.commit_resolution.git_properties.InvalidGitPropertiesException
+import com.teamscale.jacoco.agent.options.AgentOptionParseException
+import com.teamscale.jacoco.agent.options.AgentOptionsParser
+import com.teamscale.jacoco.agent.upload.UploaderException
+import com.teamscale.jacoco.agent.upload.artifactory.ArtifactoryConfig.Companion.ARTIFACTORY_API_KEY_OPTION
+import com.teamscale.jacoco.agent.upload.artifactory.ArtifactoryConfig.Companion.ARTIFACTORY_LEGACY_PATH_OPTION
+import com.teamscale.jacoco.agent.upload.artifactory.ArtifactoryConfig.Companion.ARTIFACTORY_PARTITION
+import com.teamscale.jacoco.agent.upload.artifactory.ArtifactoryConfig.Companion.ARTIFACTORY_PASSWORD_OPTION
+import com.teamscale.jacoco.agent.upload.artifactory.ArtifactoryConfig.Companion.ARTIFACTORY_PATH_SUFFIX
+import com.teamscale.jacoco.agent.upload.artifactory.ArtifactoryConfig.Companion.ARTIFACTORY_USER_OPTION
+import com.teamscale.jacoco.agent.upload.artifactory.ArtifactoryConfig.Companion.ARTIFACTORY_ZIP_PATH_OPTION
+import okhttp3.HttpUrl
+import java.io.File
+import java.io.IOException
+import java.time.format.DateTimeFormatter
+
+/** Config necessary to upload files to an azure file storage. */
+class ArtifactoryConfig {
+ /** Related to [ARTIFACTORY_USER_OPTION] */
+ @JvmField
+ var url: HttpUrl? = null
+
+ /** Related to [ARTIFACTORY_USER_OPTION] */
+ @JvmField
+ var user: String? = null
+
+ /** Related to [ARTIFACTORY_PASSWORD_OPTION] */
+ @JvmField
+ var password: String? = null
+
+ /** Related to [ARTIFACTORY_LEGACY_PATH_OPTION] */
+ var legacyPath: Boolean = false
+
+ /** Related to [ARTIFACTORY_ZIP_PATH_OPTION] */
+ var zipPath: String? = null
+
+ /** Related to [ARTIFACTORY_PATH_SUFFIX] */
+ var pathSuffix: String? = null
+
+ /** The information regarding a commit. */
+ @JvmField
+ var commitInfo: CommitInfo? = null
+
+ /** Related to [ARTIFACTORY_API_KEY_OPTION] */
+ @JvmField
+ var apiKey: String? = null
+
+ /** Related to [ARTIFACTORY_PARTITION] */
+ @JvmField
+ var partition: String? = null
+
+ /** Checks if all required options are set to upload to artifactory. */
+ fun hasAllRequiredFieldsSet(): Boolean {
+ val requiredAuthOptionsSet = (user != null && password != null) || apiKey != null
+ val partitionSet = partition != null || legacyPath
+ return url != null && partitionSet && requiredAuthOptionsSet
+ }
+
+ /** Checks if all required fields are null. */
+ fun hasAllRequiredFieldsNull() = url == null && user == null && password == null && apiKey == null && partition == null
+
+ /** Checks whether commit and revision are set. */
+ fun hasCommitInfo() = commitInfo != null
+
+ companion object {
+ /**
+ * Option to specify the artifactory URL. This shall be the entire path down to the directory to which the coverage
+ * should be uploaded to, not only the base url of artifactory.
+ */
+ const val ARTIFACTORY_URL_OPTION: String = "artifactory-url"
+
+ /**
+ * Username that shall be used for basic auth. Alternative to basic auth is to use an API key with the
+ * [ARTIFACTORY_API_KEY_OPTION]
+ */
+ const val ARTIFACTORY_USER_OPTION: String = "artifactory-user"
+
+ /**
+ * Password that shall be used for basic auth. Alternative to basic auth is to use an API key with the
+ * [ARTIFACTORY_API_KEY_OPTION]
+ */
+ const val ARTIFACTORY_PASSWORD_OPTION: String = "artifactory-password"
+
+ /**
+ * API key that shall be used to authenticate requests to artifactory with the
+ * [ArtifactoryUploader.ARTIFACTORY_API_HEADER]. Alternatively
+ * basic auth with username ([ARTIFACTORY_USER_OPTION]) and password
+ * ([ARTIFACTORY_PASSWORD_OPTION]) can be used.
+ */
+ const val ARTIFACTORY_API_KEY_OPTION: String = "artifactory-api-key"
+
+ /**
+ * Option that specifies if the legacy path for uploading files to artifactory should be used instead of the new
+ * standard path.
+ */
+ const val ARTIFACTORY_LEGACY_PATH_OPTION: String = "artifactory-legacy-path"
+
+ /**
+ * Option that specifies under which path the coverage file shall lie within the zip file that is created for the
+ * upload.
+ */
+ const val ARTIFACTORY_ZIP_PATH_OPTION: String = "artifactory-zip-path"
+
+ /**
+ * Option that specifies intermediate directories which should be appended.
+ */
+ const val ARTIFACTORY_PATH_SUFFIX: String = "artifactory-path-suffix"
+
+ /**
+ * Specifies the location of the JAR file which includes the git.properties file.
+ */
+ const val ARTIFACTORY_GIT_PROPERTIES_JAR_OPTION: String = "artifactory-git-properties-jar"
+
+ /**
+ * Specifies the date format in which the commit timestamp in the git.properties file is formatted.
+ */
+ const val ARTIFACTORY_GIT_PROPERTIES_COMMIT_DATE_FORMAT_OPTION: String =
+ "artifactory-git-properties-commit-date-format"
+
+ /**
+ * Specifies the partition for which the upload is.
+ */
+ const val ARTIFACTORY_PARTITION: String = "artifactory-partition"
+
+ /**
+ * Handles all command-line options prefixed with 'artifactory-'
+ *
+ * @return true if it has successfully processed the given option.
+ */
+ @JvmStatic
+ @Throws(AgentOptionParseException::class)
+ fun handleArtifactoryOptions(options: ArtifactoryConfig, key: String, value: String): Boolean {
+ when (key) {
+ ARTIFACTORY_URL_OPTION -> {
+ options.url = AgentOptionsParser.parseUrl(key, value)
+ return true
+ }
+ ARTIFACTORY_USER_OPTION -> {
+ options.user = value
+ return true
+ }
+ ARTIFACTORY_PASSWORD_OPTION -> {
+ options.password = value
+ return true
+ }
+ ARTIFACTORY_LEGACY_PATH_OPTION -> {
+ options.legacyPath = value.toBoolean()
+ return true
+ }
+ ARTIFACTORY_ZIP_PATH_OPTION -> {
+ options.zipPath = stripSuffix(value, "/")
+ return true
+ }
+ ARTIFACTORY_PATH_SUFFIX -> {
+ options.pathSuffix = stripSuffix(value, "/")
+ return true
+ }
+ ARTIFACTORY_API_KEY_OPTION -> {
+ options.apiKey = value
+ return true
+ }
+ ARTIFACTORY_PARTITION -> {
+ options.partition = value
+ return true
+ }
+ else -> return false
+ }
+ }
+
+ /** Parses the commit information form a git.properties file. */
+ @JvmStatic
+ @Throws(UploaderException::class)
+ fun parseGitProperties(
+ jarFile: File, searchRecursively: Boolean, gitPropertiesCommitTimeFormat: DateTimeFormatter?
+ ): CommitInfo? {
+ try {
+ val commitInfo = GitPropertiesLocatorUtils.getCommitInfoFromGitProperties(
+ jarFile,
+ true,
+ searchRecursively,
+ gitPropertiesCommitTimeFormat
+ )
+ if (commitInfo.isEmpty()) {
+ throw UploaderException("Found no git.properties files in $jarFile")
+ }
+ if (commitInfo.size > 1) {
+ throw UploaderException(
+ ("Found multiple git.properties files in " + jarFile
+ + ". Uploading to multiple projects is currently not possible with Artifactory. "
+ + "Please contact CQSE if you need this feature.")
+ )
+ }
+ return commitInfo.firstOrNull()
+ } catch (e: IOException) {
+ throw UploaderException("Could not locate a valid git.properties file in $jarFile", e)
+ } catch (e: InvalidGitPropertiesException) {
+ throw UploaderException("Could not locate a valid git.properties file in $jarFile", e)
+ }
+ }
+ }
+}
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
new file mode 100644
index 000000000..885ba65d4
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/artifactory/ArtifactoryUploader.kt
@@ -0,0 +1,145 @@
+package com.teamscale.jacoco.agent.upload.artifactory
+
+import com.teamscale.client.CommitDescriptor.Companion.parse
+import com.teamscale.client.EReportFormat
+import com.teamscale.client.FileSystemUtils.normalizeSeparators
+import com.teamscale.client.FileSystemUtils.replaceFilePathFilenameWith
+import com.teamscale.client.HttpUtils.getBasicAuthInterceptor
+import com.teamscale.client.StringUtils.emptyToNull
+import com.teamscale.client.StringUtils.nullToEmpty
+import com.teamscale.jacoco.agent.commit_resolution.git_properties.CommitInfo
+import com.teamscale.jacoco.agent.upload.HttpZipUploaderBase
+import com.teamscale.jacoco.agent.upload.IUploadRetry
+import com.teamscale.jacoco.agent.upload.teamscale.ETeamscaleServerProperties
+import com.teamscale.jacoco.agent.upload.teamscale.TeamscaleUploader
+import com.teamscale.report.jacoco.CoverageFile
+import okhttp3.Interceptor
+import okhttp3.OkHttpClient
+import okhttp3.ResponseBody
+import retrofit2.Response
+import java.io.File
+import java.io.FileWriter
+import java.io.IOException
+import java.nio.file.Path
+import java.util.*
+import kotlin.Throws
+
+/**
+ * Uploads XMLs to Artifactory.
+ */
+class ArtifactoryUploader(
+ private val artifactoryConfig: ArtifactoryConfig,
+ additionalMetaDataFiles: MutableList,
+ reportFormat: EReportFormat
+) : HttpZipUploaderBase(
+ artifactoryConfig.url!!,
+ additionalMetaDataFiles,
+ IArtifactoryUploadApi::class.java
+), IUploadRetry {
+ private val coverageFormat = reportFormat.name.lowercase(Locale.getDefault())
+ private var uploadPath: String? = null
+
+ override fun markFileForUploadRetry(coverageFile: CoverageFile) {
+ val uploadMetadataFile = File(
+ replaceFilePathFilenameWith(
+ normalizeSeparators(coverageFile.toString()),
+ "${coverageFile.name}${TeamscaleUploader.RETRY_UPLOAD_FILE_SUFFIX}"
+ )
+ )
+ val properties = createArtifactoryProperties()
+ try {
+ FileWriter(uploadMetadataFile).use { writer ->
+ properties.store(writer, null)
+ }
+ } catch (_: IOException) {
+ logger.warn(
+ "Failed to create metadata file for automatic upload retry of {}. Please manually retry the coverage upload to Azure.",
+ coverageFile
+ )
+ uploadMetadataFile.delete()
+ }
+ }
+
+ override fun reupload(coverageFile: CoverageFile, properties: Properties) {
+ val config = ArtifactoryConfig()
+ config.url = artifactoryConfig.url
+ config.user = artifactoryConfig.user
+ config.password = artifactoryConfig.password
+ config.legacyPath = artifactoryConfig.legacyPath
+ config.zipPath = artifactoryConfig.zipPath
+ config.pathSuffix = artifactoryConfig.pathSuffix
+ val revision = properties.getProperty(ETeamscaleServerProperties.REVISION.name)
+ val commitString = properties.getProperty(ETeamscaleServerProperties.COMMIT.name)
+ config.commitInfo = CommitInfo(revision, parse(commitString))
+ config.apiKey = artifactoryConfig.apiKey
+ config.partition = emptyToNull(properties.getProperty(ETeamscaleServerProperties.PARTITION.name))
+ setUploadPath(coverageFile, config)
+ super.upload(coverageFile)
+ }
+
+ /** Creates properties from the artifactory configs. */
+ private fun createArtifactoryProperties() = Properties().apply {
+ setProperty(ETeamscaleServerProperties.REVISION.name, artifactoryConfig.commitInfo!!.revision)
+ setProperty(ETeamscaleServerProperties.COMMIT.name, artifactoryConfig.commitInfo!!.commit.toString())
+ setProperty(ETeamscaleServerProperties.PARTITION.name, nullToEmpty(artifactoryConfig.partition))
+ }
+
+ override fun configureOkHttp(builder: OkHttpClient.Builder) {
+ super.configureOkHttp(builder)
+ if (artifactoryConfig.apiKey != null) {
+ builder.addInterceptor(this.artifactoryApiHeaderInterceptor)
+ } else {
+ builder.addInterceptor(
+ getBasicAuthInterceptor(artifactoryConfig.user!!, artifactoryConfig.password!!)
+ )
+ }
+ }
+
+ private fun setUploadPath(coverageFile: CoverageFile, artifactoryConfig: ArtifactoryConfig) {
+ val commit = artifactoryConfig.commitInfo?.commit ?: return
+ val revision = artifactoryConfig.commitInfo?.revision ?: return
+ val timeRev = "${commit.timestamp}-${revision}"
+ val fileName = "${coverageFile.nameWithoutExtension}.zip"
+
+ uploadPath = if (artifactoryConfig.legacyPath) {
+ "${commit.branchName}/$timeRev/$fileName"
+ } else {
+ val suffixSegment = artifactoryConfig.pathSuffix?.let { "$it/" } ?: ""
+ "uploads/${commit.branchName}/$timeRev/${artifactoryConfig.partition}/$coverageFormat/$suffixSegment$fileName"
+ }
+ }
+
+ override fun upload(coverageFile: CoverageFile) {
+ setUploadPath(coverageFile, this.artifactoryConfig)
+ super.upload(coverageFile)
+ }
+
+ @Throws(IOException::class)
+ override fun uploadCoverageZip(coverageFile: File): Response =
+ api.uploadCoverageZip(uploadPath!!, coverageFile)
+
+ override fun getZipEntryCoverageFileName(coverageFile: CoverageFile): String {
+ var path = coverageFile.name
+ artifactoryConfig.zipPath?.let { path = "$it/$path" }
+ return path
+ }
+
+ /** {@inheritDoc} */
+ override fun describe() = "Uploading to $uploadUrl"
+
+ private val artifactoryApiHeaderInterceptor: Interceptor
+ get() = Interceptor { chain ->
+ val newRequest = chain.request().newBuilder()
+ .header(ARTIFACTORY_API_HEADER, artifactoryConfig.apiKey!!)
+ .build()
+ chain.proceed(newRequest)
+ }
+
+ companion object {
+ /**
+ * Header that can be used as alternative to basic authentication to authenticate requests against artifactory. For
+ * details check https://www.jfrog.com/confluence/display/JFROG/Artifactory+REST+API
+ */
+ const val ARTIFACTORY_API_HEADER: String = "X-JFrog-Art-Api"
+ }
+}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/artifactory/IArtifactoryUploadApi.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/artifactory/IArtifactoryUploadApi.kt
new file mode 100644
index 000000000..1c25abcf6
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/artifactory/IArtifactoryUploadApi.kt
@@ -0,0 +1,29 @@
+package com.teamscale.jacoco.agent.upload.artifactory
+
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.RequestBody
+import okhttp3.RequestBody.Companion.create
+import okhttp3.ResponseBody
+import retrofit2.Call
+import retrofit2.Response
+import retrofit2.http.Body
+import retrofit2.http.PUT
+import retrofit2.http.Path
+import java.io.File
+import java.io.IOException
+
+/** [retrofit2.Retrofit] API specification for the [ArtifactoryUploader]. */
+interface IArtifactoryUploadApi {
+ /** The upload API call. */
+ @PUT("{path}")
+ fun upload(@Path("path") path: String, @Body uploadedFile: RequestBody): Call
+
+ /**
+ * Convenience method to perform an upload for a coverage zip.
+ */
+ @Throws(IOException::class)
+ fun uploadCoverageZip(path: String, data: File): Response {
+ val body = create("application/zip".toMediaTypeOrNull(), data)
+ return upload(path, body).execute()
+ }
+}
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
new file mode 100644
index 000000000..b71e5a564
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/delay/DelayedUploader.kt
@@ -0,0 +1,113 @@
+package com.teamscale.jacoco.agent.upload.delay
+
+import com.teamscale.jacoco.agent.logging.LoggingUtils.getLogger
+import com.teamscale.jacoco.agent.upload.IUploader
+import com.teamscale.jacoco.agent.util.DaemonThreadFactory
+import com.teamscale.report.jacoco.CoverageFile
+import java.io.IOException
+import java.nio.file.Files
+import java.nio.file.Path
+import java.util.concurrent.Executor
+import java.util.concurrent.Executors
+import java.util.function.Function
+import kotlin.io.path.isDirectory
+import kotlin.io.path.walk
+
+/**
+ * Wraps an [IUploader] and in order to delay upload until a all
+ * information describing a commit is asynchronously made available.
+ */
+class DelayedUploader internal constructor(
+ private val wrappedUploaderFactory: Function,
+ private val cacheDir: Path,
+ private val executor: Executor
+) : IUploader {
+ private val logger = getLogger(this)
+ private var wrappedUploader: IUploader? = null
+
+ constructor(wrappedUploaderFactory: Function, cacheDir: Path) : this(
+ wrappedUploaderFactory, cacheDir, Executors.newSingleThreadExecutor(
+ DaemonThreadFactory(DelayedUploader::class.java, "Delayed cache upload thread")
+ )
+ )
+
+ /**
+ * Visible for testing. Allows tests to control the [Executor] to test the
+ * asynchronous functionality of this class.
+ */
+ /* package */
+ init {
+ registerShutdownHook()
+ }
+
+ private fun registerShutdownHook() {
+ Runtime.getRuntime().addShutdownHook(Thread {
+ if (wrappedUploader == null) {
+ logger.error(
+ ("The application was shut down before a commit could be found. The recorded coverage"
+ + " is still cached in {} but will not be automatically processed. You configured the"
+ + " agent to auto-detect the commit to which the recorded coverage should be uploaded to"
+ + " Teamscale. In order to fix this problem, you need to provide a git.properties file"
+ + " in all of the profiled Jar/War/Ear/... files. If you're using Gradle or"
+ + " Maven, you can use a plugin to create a proper git.properties file for you, see"
+ + " https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto-git-info"
+ + "\nTo debug problems with git.properties, please enable debug logging for the agent via"
+ + " the logging-config parameter."), cacheDir.toAbsolutePath()
+ )
+ }
+ })
+ }
+
+ @Synchronized
+ override fun upload(coverageFile: CoverageFile) {
+ if (wrappedUploader == null) {
+ logger.info(
+ "The commit to upload to has not yet been found. Caching coverage XML in {}",
+ cacheDir.toAbsolutePath()
+ )
+ } else {
+ wrappedUploader?.upload(coverageFile)
+ }
+ }
+
+ override fun describe() =
+ wrappedUploader?.describe() ?: "Temporary cache until commit is resolved: ${cacheDir.toAbsolutePath()}"
+
+ /**
+ * Sets the commit to upload the XMLs to and asynchronously triggers the upload
+ * of all cached XMLs. This method should only be called once.
+ */
+ @Synchronized
+ fun setCommitAndTriggerAsynchronousUpload(information: T) {
+ if (wrappedUploader == null) {
+ wrappedUploader = wrappedUploaderFactory.apply(information)
+ logger.info(
+ "Commit to upload to has been found: {}. Uploading any cached XMLs now to {}", information,
+ wrappedUploader?.describe()
+ )
+ 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.",
+ wrappedUploader?.describe(), information
+ )
+ }
+ }
+
+ private fun uploadCachedXmls() {
+ try {
+ if (!cacheDir.isDirectory()) {
+ // Found data before XML was dumped
+ return
+ }
+ val xmlFilesStream = cacheDir.walk().filter { path ->
+ val fileName = path.fileName.toString()
+ fileName.startsWith("jacoco-") && fileName.endsWith(".xml")
+ }
+ xmlFilesStream.forEach { path -> wrappedUploader?.upload(CoverageFile(path.toFile())) }
+ logger.debug("Finished upload of cached XMLs to {}", wrappedUploader?.describe())
+ } catch (e: IOException) {
+ logger.error("Failed to list cached coverage XML files in {}", cacheDir.toAbsolutePath(), e)
+ }
+ }
+}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/teamscale/DelayedTeamscaleMultiProjectUploader.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/teamscale/DelayedTeamscaleMultiProjectUploader.kt
new file mode 100644
index 000000000..c839ecfe7
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/teamscale/DelayedTeamscaleMultiProjectUploader.kt
@@ -0,0 +1,40 @@
+package com.teamscale.jacoco.agent.upload.teamscale
+
+import com.teamscale.client.TeamscaleServer
+import com.teamscale.jacoco.agent.commit_resolution.git_properties.CommitInfo
+import com.teamscale.jacoco.agent.options.ProjectAndCommit
+import com.teamscale.jacoco.agent.upload.DelayedMultiUploaderBase
+import com.teamscale.jacoco.agent.upload.IUploader
+import java.io.File
+import java.util.function.BiFunction
+
+/** Wrapper for [TeamscaleUploader] that allows to upload the same coverage file to multiple Teamscale projects. */
+class DelayedTeamscaleMultiProjectUploader(
+ private val teamscaleServerFactory: BiFunction
+) : DelayedMultiUploaderBase(), IUploader {
+ @JvmField
+ val teamscaleUploaders = mutableListOf()
+
+ /**
+ * Adds a teamscale project and commit as a possible new target to upload coverage to. Checks if the project and
+ * commit are already registered as an upload target and will prevent duplicate uploads.
+ */
+ fun addTeamscaleProjectAndCommit(file: File, projectAndCommit: ProjectAndCommit) {
+ val teamscaleServer = teamscaleServerFactory.apply(
+ projectAndCommit.project,
+ projectAndCommit.commitInfo
+ )
+
+ if (teamscaleUploaders.any { it.teamscaleServer.hasSameProjectAndCommit(teamscaleServer) }) {
+ logger.debug(
+ "Project and commit in git.properties file {} are already registered as upload target. Coverage will not be uploaded multiple times to the same project {} and commit info {}.",
+ file, projectAndCommit.project, projectAndCommit.commitInfo
+ )
+ return
+ }
+ teamscaleUploaders.add(TeamscaleUploader(teamscaleServer))
+ }
+
+ override val wrappedUploaders: MutableCollection
+ get() = teamscaleUploaders.toMutableList()
+}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/teamscale/ETeamscaleServerProperties.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/teamscale/ETeamscaleServerProperties.kt
new file mode 100644
index 000000000..7dfbc8eb0
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/teamscale/ETeamscaleServerProperties.kt
@@ -0,0 +1,23 @@
+package com.teamscale.jacoco.agent.upload.teamscale
+
+/** Describes all the fields of the [com.teamscale.client.TeamscaleServer]. */
+enum class ETeamscaleServerProperties {
+ /** See [com.teamscale.client.TeamscaleServer.url] */
+ URL,
+ /** See [com.teamscale.client.TeamscaleServer.project] */
+ PROJECT,
+ /** See [com.teamscale.client.TeamscaleServer.userName] */
+ USER_NAME,
+ /** See [com.teamscale.client.TeamscaleServer.userAccessToken] */
+ USER_ACCESS_TOKEN,
+ /** See [com.teamscale.client.TeamscaleServer.partition] */
+ PARTITION,
+ /** See [com.teamscale.client.TeamscaleServer.commit] */
+ COMMIT,
+ /** See [com.teamscale.client.TeamscaleServer.revision] */
+ REVISION,
+ /** See [com.teamscale.client.TeamscaleServer.repository] */
+ REPOSITORY,
+ /** See [com.teamscale.client.TeamscaleServer.message] */
+ MESSAGE
+}
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
new file mode 100644
index 000000000..a8aff8869
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/upload/teamscale/TeamscaleUploader.kt
@@ -0,0 +1,150 @@
+package com.teamscale.jacoco.agent.upload.teamscale
+
+import com.teamscale.client.*
+import com.teamscale.client.CommitDescriptor.Companion.parse
+import com.teamscale.client.FileSystemUtils.normalizeSeparators
+import com.teamscale.client.FileSystemUtils.replaceFilePathFilenameWith
+import com.teamscale.client.StringUtils.emptyToNull
+import com.teamscale.client.StringUtils.nullToEmpty
+import com.teamscale.jacoco.agent.benchmark
+import com.teamscale.jacoco.agent.logging.LoggingUtils.getLogger
+import com.teamscale.jacoco.agent.upload.IUploadRetry
+import com.teamscale.jacoco.agent.upload.IUploader
+import com.teamscale.jacoco.agent.util.AgentUtils
+import com.teamscale.jacoco.agent.util.Benchmark
+import com.teamscale.report.jacoco.CoverageFile
+import java.io.File
+import java.io.IOException
+import java.io.OutputStreamWriter
+import java.nio.charset.StandardCharsets
+import java.nio.file.Files
+import java.util.*
+
+/** Uploads XML Coverage to a Teamscale instance. */
+class TeamscaleUploader(
+ @JvmField val teamscaleServer: TeamscaleServer
+) : IUploader, IUploadRetry {
+ private val logger = getLogger(this)
+
+ override fun upload(coverageFile: CoverageFile) {
+ doUpload(coverageFile, teamscaleServer)
+ }
+
+ override fun reupload(coverageFile: CoverageFile, properties: Properties) {
+ val server = TeamscaleServer()
+ server.project = properties.getProperty(ETeamscaleServerProperties.PROJECT.name)
+ server.commit = parse(properties.getProperty(ETeamscaleServerProperties.COMMIT.name))
+ server.partition = properties.getProperty(ETeamscaleServerProperties.PARTITION.name)
+ server.revision = emptyToNull(properties.getProperty(ETeamscaleServerProperties.REVISION.name))
+ server.repository = emptyToNull(properties.getProperty(ETeamscaleServerProperties.REPOSITORY.name))
+ server.userAccessToken = teamscaleServer.userAccessToken
+ server.userName = teamscaleServer.userName
+ server.url = teamscaleServer.url
+ server.message = properties.getProperty(ETeamscaleServerProperties.MESSAGE.name)
+ doUpload(coverageFile, server)
+ }
+
+ private fun doUpload(coverageFile: CoverageFile, teamscaleServer: TeamscaleServer) {
+ benchmark("Uploading report to Teamscale") {
+ if (tryUploading(coverageFile, teamscaleServer)) {
+ deleteCoverageFile(coverageFile)
+ } else {
+ logger.warn(
+ ("Failed to upload coverage to Teamscale. "
+ + "Won't delete local file {} so that the upload can automatically be retried upon profiler restart. "
+ + "Upload can also be retried manually."), coverageFile
+ )
+ markFileForUploadRetry(coverageFile)
+ }
+ }
+ }
+
+ override fun markFileForUploadRetry(coverageFile: CoverageFile) {
+ val uploadMetadataFile = File(
+ replaceFilePathFilenameWith(
+ normalizeSeparators(coverageFile.toString()),
+ coverageFile.name + RETRY_UPLOAD_FILE_SUFFIX
+ )
+ )
+ val serverProperties = this.createServerProperties()
+ try {
+ OutputStreamWriter(
+ Files.newOutputStream(uploadMetadataFile.toPath()),
+ StandardCharsets.UTF_8
+ ).use { writer ->
+ serverProperties.store(writer, null)
+ }
+ } catch (_: IOException) {
+ logger.warn(
+ "Failed to create metadata file for automatic upload retry of {}. Please manually retry the coverage upload to Teamscale.",
+ coverageFile
+ )
+ uploadMetadataFile.delete()
+ }
+ }
+
+ /**
+ * Creates server properties to be written in a properties file.
+ */
+ private fun createServerProperties() = Properties().apply {
+ setProperty(ETeamscaleServerProperties.PROJECT.name, teamscaleServer.project)
+ setProperty(ETeamscaleServerProperties.PARTITION.name, teamscaleServer.partition)
+ if (teamscaleServer.commit != null) {
+ setProperty(ETeamscaleServerProperties.COMMIT.name, teamscaleServer.commit.toString())
+ }
+ setProperty(ETeamscaleServerProperties.REVISION.name, nullToEmpty(teamscaleServer.revision))
+ setProperty(
+ ETeamscaleServerProperties.REPOSITORY.name,
+ nullToEmpty(teamscaleServer.repository)
+ )
+ setProperty(ETeamscaleServerProperties.MESSAGE.name, teamscaleServer.message)
+ }
+
+ private fun deleteCoverageFile(coverageFile: CoverageFile) {
+ try {
+ coverageFile.delete()
+ } catch (e: IOException) {
+ logger.warn(
+ "The upload to Teamscale was successful, but the deletion of the coverage file {} failed. "
+ + "You can delete it yourself anytime - it is no longer needed.", coverageFile, e
+ )
+ }
+ }
+
+ /** Performs the upload and returns `true` if successful. */
+ private fun tryUploading(coverageFile: CoverageFile, teamscaleServer: TeamscaleServer): Boolean {
+ logger.debug("Uploading JaCoCo artifact to {}", teamscaleServer)
+
+ try {
+ // Cannot be executed in the constructor as this causes issues in WildFly server
+ // (See #100)
+ TeamscaleServiceGenerator.createService(
+ teamscaleServer.url!!,
+ teamscaleServer.userName!!,
+ teamscaleServer.userAccessToken!!,
+ AgentUtils.USER_AGENT
+ ).uploadReport(
+ teamscaleServer.project!!,
+ teamscaleServer.commit,
+ teamscaleServer.revision,
+ teamscaleServer.repository, teamscaleServer.partition!!, EReportFormat.JACOCO,
+ teamscaleServer.message!!, coverageFile.createFormRequestBody()
+ )
+ return true
+ } catch (e: IOException) {
+ logger.error("Failed to upload coverage to {}", teamscaleServer, e)
+ return false
+ }
+ }
+
+ override fun describe(): String {
+ return "Uploading to " + teamscaleServer
+ }
+
+ companion object {
+ /**
+ * The properties file suffix for unsuccessful coverage uploads.
+ */
+ const val RETRY_UPLOAD_FILE_SUFFIX: String = "_upload-retry.properties"
+ }
+}
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
new file mode 100644
index 000000000..6f5eb0d84
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/util/AgentUtils.kt
@@ -0,0 +1,57 @@
+package com.teamscale.jacoco.agent.util
+
+import com.teamscale.client.FileSystemUtils
+import com.teamscale.client.TeamscaleServiceGenerator
+import com.teamscale.jacoco.agent.PreMain
+import com.teamscale.jacoco.agent.configuration.ProcessInformationRetriever
+import java.io.IOException
+import java.net.URISyntaxException
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.util.*
+
+/** General utilities for working with the agent. */
+object AgentUtils {
+ /** Version of this program. */
+ val VERSION: String
+
+ /** User-Agent header value for HTTP requests. */
+ @JvmField
+ val USER_AGENT: String
+
+ /**
+ * Returns the main temporary directory where all agent temp files should be placed.
+ */
+ @JvmStatic
+ val mainTempDirectory: Path by lazy {
+ try {
+ // We add a trailing hyphen here to visually separate the PID from the random number that Java appends
+ // to the name to make it unique
+ Files.createTempDirectory(
+ "teamscale-java-profiler-${FileSystemUtils.toSafeFilename(ProcessInformationRetriever.pID)}-"
+ )
+ } catch (e: IOException) {
+ throw RuntimeException("Failed to create temporary directory for agent files", e)
+ }
+ }
+
+ /** Returns the directory that contains the agent installation. */
+ @JvmStatic
+ val agentDirectory: Path by lazy {
+ try {
+ val jarFileUri = PreMain::class.java.getProtectionDomain().codeSource.location.toURI()
+ // we assume that the dist zip is extracted and the agent jar not moved
+ val jarDirectory = Paths.get(jarFileUri).parent
+ jarDirectory.parent ?: jarDirectory // happens when the jar file is stored in the root directory
+ } catch (e: URISyntaxException) {
+ throw RuntimeException("Failed to obtain agent directory. This is a bug, please report it.", e)
+ }
+ }
+
+ init {
+ val bundle = ResourceBundle.getBundle("com.teamscale.jacoco.agent.app")
+ VERSION = bundle.getString("version")
+ USER_AGENT = TeamscaleServiceGenerator.buildUserAgent("Teamscale Java Profiler", VERSION)
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/util/Assertions.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/util/Assertions.kt
new file mode 100644
index 000000000..ce9b0e44f
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/util/Assertions.kt
@@ -0,0 +1,48 @@
+package com.teamscale.jacoco.agent.util
+
+import org.jetbrains.annotations.Contract
+
+/**
+ * Simple methods to implement assertions.
+ */
+object Assertions {
+ /**
+ * Checks if a condition is `true`.
+ *
+ * @param condition condition to check
+ * @param message exception message
+ * @throws AssertionError if the condition is `false`
+ */
+ @JvmStatic
+ @Contract(value = "false, _ -> fail", pure = true)
+ @Throws(AssertionError::class)
+ fun isTrue(condition: Boolean, message: String?) {
+ throwAssertionErrorIfTestFails(condition, message)
+ }
+
+ /**
+ * Checks if a condition is `false`.
+ *
+ * @param condition condition to check
+ * @param message exception message
+ * @throws AssertionError if the condition is `true`
+ */
+ @Contract(value = "true, _ -> fail", pure = true)
+ @Throws(AssertionError::class)
+ fun isFalse(condition: Boolean, message: String?) {
+ throwAssertionErrorIfTestFails(!condition, message)
+ }
+
+ /**
+ * Throws an [AssertionError] if the test fails.
+ *
+ * @param test test which should be true
+ * @param message exception message
+ * @throws AssertionError if the test fails
+ */
+ private fun throwAssertionErrorIfTestFails(test: Boolean, message: String?) {
+ if (!test) {
+ throw AssertionError(message)
+ }
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/util/DaemonThreadFactory.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/util/DaemonThreadFactory.kt
new file mode 100644
index 000000000..ac4beb65b
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/util/DaemonThreadFactory.kt
@@ -0,0 +1,15 @@
+package com.teamscale.jacoco.agent.util
+
+import java.util.concurrent.ThreadFactory
+
+/**
+ * [java.util.concurrent.ThreadFactory] that only produces deamon threads (threads that don't prevent JVM shutdown) with a fixed name.
+ */
+class DaemonThreadFactory(owningClass: Class<*>, threadName: String?) : ThreadFactory {
+ private val threadName = "Teamscale Java Profiler ${owningClass.getSimpleName()} $threadName"
+
+ override fun newThread(runnable: Runnable) =
+ Thread(runnable, threadName).apply {
+ setDaemon(true)
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/util/NullOutputStream.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/util/NullOutputStream.kt
new file mode 100644
index 000000000..6f15a8e6b
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/util/NullOutputStream.kt
@@ -0,0 +1,20 @@
+package com.teamscale.jacoco.agent.util
+
+import java.io.IOException
+import java.io.OutputStream
+
+/** NOP output stream implementation. */
+class NullOutputStream : OutputStream() {
+ override fun write(b: ByteArray, off: Int, len: Int) {
+ // to /dev/null
+ }
+
+ override fun write(b: Int) {
+ // to /dev/null
+ }
+
+ @Throws(IOException::class)
+ override fun write(b: ByteArray) {
+ // to /dev/null
+ }
+}
\ No newline at end of file
diff --git a/agent/src/test/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitMultiProjectPropertiesLocatorTest.java b/agent/src/test/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitMultiProjectPropertiesLocatorTest.java
index ec38ee8b6..b638f7170 100644
--- a/agent/src/test/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitMultiProjectPropertiesLocatorTest.java
+++ b/agent/src/test/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitMultiProjectPropertiesLocatorTest.java
@@ -27,7 +27,7 @@ void testNoErrorIsThrownWhenGitPropertiesFileDoesNotHaveAProject() {
File jarFile = new File(getClass().getResource("emptyTeamscaleProjectGitProperties").getFile());
locator.searchFile(jarFile, false);
assertThat(projectAndCommits.size()).isEqualTo(1);
- assertThat(projectAndCommits.get(0).getProject()).isEqualTo("my-teamscale-project");
+ assertThat(projectAndCommits.get(0).project).isEqualTo("my-teamscale-project");
}
@Test
@@ -45,8 +45,8 @@ void testNoMultipleUploadsToSameProjectAndRevision() {
);
File jarFile = new File(getClass().getResource("multiple-same-target-git-properties-folder").getFile());
locator.searchFile(jarFile, false);
- List teamscaleServers = delayedTeamscaleMultiProjectUploader.getTeamscaleUploaders().stream()
- .map(TeamscaleUploader::getTeamscaleServer).collect(Collectors.toList());
+ List teamscaleServers = delayedTeamscaleMultiProjectUploader.teamscaleUploaders.stream()
+ .map(t -> t.teamscaleServer).collect(Collectors.toList());
assertThat(teamscaleServers).hasSize(2);
assertThat(teamscaleServers).anyMatch(server -> server.project.equals("demo2") && server.commit.equals(
new CommitDescriptor("master", "1645713803000")));
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 7e6127601..859debcc4 100644
--- a/teamscale-client/src/main/kotlin/com/teamscale/client/FileSystemUtils.kt
+++ b/teamscale-client/src/main/kotlin/com/teamscale/client/FileSystemUtils.kt
@@ -236,9 +236,9 @@ object FileSystemUtils {
@Throws(IOException::class)
fun readProperties(propertiesFile: File): Properties {
propertiesFile.inputStream().use { stream ->
- val props = Properties()
- props.load(stream)
- return props
+ return Properties().apply {
+ load(stream)
+ }
}
}
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 e69f51e90..d121cec0b 100644
--- a/teamscale-client/src/main/kotlin/com/teamscale/client/HttpUtils.kt
+++ b/teamscale-client/src/main/kotlin/com/teamscale/client/HttpUtils.kt
@@ -61,8 +61,9 @@ object HttpUtils {
@JvmOverloads
@JvmStatic
fun createRetrofit(
- retrofitBuilderAction: Consumer,
- okHttpBuilderAction: Consumer, readTimeout: Duration = DEFAULT_READ_TIMEOUT,
+ retrofitBuilderAction: Retrofit.Builder.() -> Unit,
+ okHttpBuilderAction: Builder.() -> Unit,
+ readTimeout: Duration = DEFAULT_READ_TIMEOUT,
writeTimeout: Duration = DEFAULT_WRITE_TIMEOUT
): Retrofit {
val httpClientBuilder = Builder().apply {
@@ -70,10 +71,10 @@ object HttpUtils {
setUpSslValidation()
setUpProxyServer()
}
- okHttpBuilderAction.accept(httpClientBuilder)
+ okHttpBuilderAction(httpClientBuilder)
val builder = Retrofit.Builder().client(httpClientBuilder.build())
- retrofitBuilderAction.accept(builder)
+ retrofitBuilderAction(builder)
return builder.build()
}
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 314283eab..c7e52e018 100644
--- a/teamscale-client/src/main/kotlin/com/teamscale/client/ITeamscaleService.kt
+++ b/teamscale-client/src/main/kotlin/com/teamscale/client/ITeamscaleService.kt
@@ -159,7 +159,7 @@ interface ITeamscaleService {
@POST("api/v2024.7.0/profilers/{profilerId}/logs")
fun postProfilerLog(
@Path("profilerId") profilerId: String,
- @Body logEntries: List?
+ @Body logEntries: List?
): Call
}
diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/StringUtils.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/StringUtils.kt
index 64800f45d..3e2f1169d 100644
--- a/teamscale-client/src/main/kotlin/com/teamscale/client/StringUtils.kt
+++ b/teamscale-client/src/main/kotlin/com/teamscale/client/StringUtils.kt
@@ -139,7 +139,7 @@ object StringUtils {
* list is returned.
*/
@JvmStatic
- fun splitLinesAsList(content: String?): List = content?.lines() ?: emptyList()
+ fun splitLinesAsList(content: String?) = content?.lines() ?: emptyList()
/**
* Test if a string ends with one of the provided suffixes. Returns
@@ -147,18 +147,14 @@ object StringUtils {
* for short lists of suffixes.
*/
@JvmStatic
- fun endsWithOneOf(string: String, vararg suffixes: String): Boolean {
- return suffixes.any { string.endsWith(it) }
- }
+ fun endsWithOneOf(string: String, vararg suffixes: String) = suffixes.any { string.endsWith(it) }
/**
* Removes double quotes from beginning and end (if present) and returns the new
* string.
*/
@JvmStatic
- fun removeDoubleQuotes(string: String): String {
- return string.removeSuffix("\"").removePrefix("\"")
- }
+ fun removeDoubleQuotes(string: String) = string.removeSuffix("\"").removePrefix("\"")
/**
* Converts an empty string to null. If the input string is not empty, it returns the string unmodified.
@@ -167,9 +163,7 @@ object StringUtils {
* @return `null` if the input string is empty after trimming; the original string otherwise.
*/
@JvmStatic
- fun emptyToNull(string: String): String? {
- return if (isEmpty(string)) null else string
- }
+ fun emptyToNull(string: String) = if (isEmpty(string)) null else string
/**
* Converts a nullable string to a non-null, empty string.
@@ -179,7 +173,5 @@ object StringUtils {
* @return a non-null string; either the original string or an empty string if the input was null
*/
@JvmStatic
- fun nullToEmpty(stringOrNull: String?): String {
- return stringOrNull ?: EMPTY_STRING
- }
+ fun nullToEmpty(stringOrNull: String?) = stringOrNull ?: EMPTY_STRING
}
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 4691c9bf3..c48288d60 100644
--- a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleClient.kt
+++ b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleClient.kt
@@ -34,7 +34,7 @@ open class TeamscaleClient {
val url = baseUrl?.toHttpUrlOrNull() ?: throw IllegalArgumentException("Invalid URL: $baseUrl")
this.projectId = projectId
service = TeamscaleServiceGenerator.createService(
- ITeamscaleService::class.java, url, user, accessToken, userAgent, readTimeout, writeTimeout
+ url, user, accessToken, userAgent, readTimeout, writeTimeout
)
}
@@ -53,7 +53,7 @@ open class TeamscaleClient {
val url = baseUrl?.toHttpUrlOrNull() ?: throw IllegalArgumentException("Invalid URL: $baseUrl")
this.projectId = projectId
service = TeamscaleServiceGenerator.createServiceWithRequestLogging(
- ITeamscaleService::class.java, url, user, accessToken, logfile, readTimeout, writeTimeout, userAgent
+ url, user, accessToken, logfile, readTimeout, writeTimeout, userAgent
)
}
diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServiceGenerator.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServiceGenerator.kt
index 54d925131..1597381de 100644
--- a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServiceGenerator.kt
+++ b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServiceGenerator.kt
@@ -20,10 +20,7 @@ object TeamscaleServiceGenerator {
* Generates a [Retrofit] instance for the given service, which uses basic auth to authenticate against the
* server and which sets the accept header to json.
*/
- @JvmStatic
- @JvmOverloads
- fun createService(
- serviceClass: Class,
+ inline fun createService(
baseUrl: HttpUrl,
username: String,
accessToken: String,
@@ -31,16 +28,15 @@ object TeamscaleServiceGenerator {
readTimeout: Duration = HttpUtils.DEFAULT_READ_TIMEOUT,
writeTimeout: Duration = HttpUtils.DEFAULT_WRITE_TIMEOUT,
vararg interceptors: Interceptor
- ) = createServiceWithRequestLogging(
- serviceClass, baseUrl, username, accessToken, null, readTimeout, writeTimeout, userAgent, *interceptors
+ ) = createServiceWithRequestLogging(
+ baseUrl, username, accessToken, null, readTimeout, writeTimeout, userAgent, *interceptors
)
/**
* Generates a [Retrofit] instance for the given service, which uses basic auth to authenticate against the
* server and which sets the accept-header to json. Logs requests and responses to the given logfile.
*/
- fun createServiceWithRequestLogging(
- serviceClass: Class,
+ inline fun createServiceWithRequestLogging(
baseUrl: HttpUrl,
username: String,
accessToken: String,
@@ -50,33 +46,22 @@ object TeamscaleServiceGenerator {
userAgent: String,
vararg interceptors: Interceptor
): S = HttpUtils.createRetrofit(
- { retrofitBuilder ->
- retrofitBuilder.baseUrl(baseUrl)
- .addConverterFactory(JacksonConverterFactory.create(JsonUtils.OBJECT_MAPPER))
- },
- { okHttpBuilder ->
- okHttpBuilder.addInterceptors(*interceptors)
- .addInterceptor(HttpUtils.getBasicAuthInterceptor(username, accessToken))
- .addInterceptor(AcceptJsonInterceptor())
- .addNetworkInterceptor(CustomUserAgentInterceptor(userAgent))
- logfile?.let { okHttpBuilder.addInterceptor(FileLoggingInterceptor(it)) }
+ {
+ baseUrl(baseUrl).addConverterFactory(JacksonConverterFactory.create(JsonUtils.OBJECT_MAPPER))
},
- readTimeout, writeTimeout
- ).create(serviceClass)
-
- private fun OkHttpClient.Builder.addInterceptors(
- vararg interceptors: Interceptor
- ): OkHttpClient.Builder {
- interceptors.forEach { interceptor ->
- addInterceptor(interceptor)
- }
- return this
- }
+ {
+ interceptors.forEach { addInterceptor(it) }
+ addInterceptor(HttpUtils.getBasicAuthInterceptor(username, accessToken))
+ addInterceptor(AcceptJsonInterceptor())
+ addNetworkInterceptor(CustomUserAgentInterceptor(userAgent))
+ logfile?.let { addInterceptor(FileLoggingInterceptor(it)) }
+ }, readTimeout, writeTimeout
+ ).create(S::class.java)
/**
* Sets an `Accept: application/json` header on all requests.
*/
- private class AcceptJsonInterceptor : Interceptor {
+ class AcceptJsonInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val newRequest = chain.request().newBuilder().header("Accept", "application/json").build()
diff --git a/teamscale-client/src/test/kotlin/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.kt b/teamscale-client/src/test/kotlin/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.kt
index b88e50adb..77b7fdf9c 100644
--- a/teamscale-client/src/test/kotlin/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.kt
+++ b/teamscale-client/src/test/kotlin/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.kt
@@ -59,8 +59,7 @@ internal class TeamscaleServiceGeneratorProxyServerTest {
@Throws(InterruptedException::class, IOException::class)
private fun assertProxyAuthenticationIsUsed(base64EncodedBasicAuth: String) {
- val service = createService(
- ITeamscaleService::class.java,
+ val service = createService(
"http://localhost:1337".toHttpUrl(),
"someUser", "someAccesstoken",
userAgent = buildUserAgent("Test Tool", "1.0.0")