All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.teamscale.jacoco.agent.Agent Maven / Gradle / Ivy

Go to download

JVM profiler that simplifies various aspects around recording and uploading test coverage

There is a newer version: 34.0.2
Show newest version
/*-------------------------------------------------------------------------+
|                                                                          |
| Copyright (c) 2009-2018 CQSE GmbH                                        |
|                                                                          |
+-------------------------------------------------------------------------*/
package com.teamscale.jacoco.agent;

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.UploaderException;
import com.teamscale.jacoco.agent.util.AgentUtils;
import com.teamscale.jacoco.agent.util.Benchmark;
import com.teamscale.jacoco.agent.util.FileSystemUtilsClone;
import com.teamscale.jacoco.agent.util.Timer;
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.conqat.lib.commons.string.StringUtils;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.ServerProperties;

import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.instrument.Instrumentation;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.List;
import java.util.Properties;
import java.util.stream.Stream;

import static com.teamscale.jacoco.agent.upload.teamscale.TeamscaleUploader.RETRY_UPLOAD_FILE_SUFFIX;
import static com.teamscale.jacoco.agent.util.LoggingUtils.wrap;

/**
 * A wrapper around the JaCoCo Java agent that automatically triggers a dump and XML conversion based on a time
 * interval.
 */
public class Agent extends AgentBase {

	/** Converts binary data to XML. */
	private final JaCoCoXmlReportGenerator generator;

	/** Regular dump task. */
	private Timer timer;

	/** Stores the XML files. */
	protected final IUploader uploader;

	/** Constructor. */
	public Agent(AgentOptions options, Instrumentation instrumentation)
			throws IllegalStateException, UploaderException {
		super(options);

		uploader = options.createUploader(instrumentation);
		logger.info("Upload method: {}", uploader.describe());
		retryUnsuccessfulUploads(options, uploader);
		generator = new JaCoCoXmlReportGenerator(options.getClassDirectoriesOrZips(),
				options.getLocationIncludeFilter(), options.getDuplicateClassFileBehavior(),
				options.shouldIgnoreUncoveredClasses(), wrap(logger));

		if (options.shouldDumpInIntervals()) {
			timer = new Timer(this::dumpReport, Duration.ofMinutes(options.getDumpIntervalInMinutes()));
			timer.start();
			logger.info("Dumping every {} minutes.", options.getDumpIntervalInMinutes());
		}
		if (options.getTeamscaleServerOptions().partition != null) {
			controller.setSessionId(options.getTeamscaleServerOptions().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 void retryUnsuccessfulUploads(AgentOptions options, IUploader uploader) {
		Path outputPath = options.getOutputDirectory();
		if (outputPath == null) {
			// Default fallback
			outputPath = AgentUtils.getAgentDirectory().resolve("coverage");
		}

		Path parentPath = outputPath.getParent();
		if (parentPath == null) {
			logger.error("The output path '{}' does not have a parent path. Canceling upload retry.",
					outputPath.toAbsolutePath());
			return;
		}

		List reuploadCandidates = FileSystemUtilsClone.listFilesRecursively(parentPath.toFile(),
				filepath -> filepath.getName().endsWith(RETRY_UPLOAD_FILE_SUFFIX));
		for (File file : reuploadCandidates) {
			reuploadCoverageFromPropertiesFile(file, uploader);
		}
	}

	private void reuploadCoverageFromPropertiesFile(File file, IUploader uploader) {
		logger.info("Retrying previously unsuccessful coverage upload for file {}.", file);
		try (InputStreamReader reader = new InputStreamReader(Files.newInputStream(file.toPath()),
				StandardCharsets.UTF_8)) {
			Properties properties = new Properties();
			properties.load(reader);
			CoverageFile coverageFile = new CoverageFile(
					new File(StringUtils.stripSuffix(file.getAbsolutePath(), RETRY_UPLOAD_FILE_SUFFIX)));

			if (uploader instanceof IUploadRetry) {
				((IUploadRetry) uploader).reupload(coverageFile, properties);
			} else {
				logger.info("Reupload not implemented for uploader {}", uploader.describe());
			}
			Files.deleteIfExists(file.toPath());
		} catch (IOException e) {
			logger.error("Reuploading coverage failed. " + e);
		}
	}

	@Override
	protected ResourceConfig initResourceConfig() {
		ResourceConfig resourceConfig = new ResourceConfig();
		resourceConfig.property(ServerProperties.WADL_FEATURE_DISABLE, Boolean.TRUE.toString());
		AgentResource.setAgent(this);
		return resourceConfig.register(AgentResource.class).register(GenericExceptionMapper.class);
	}

	@Override
	protected void prepareShutdown() {
		if (timer != null) {
			timer.stop();
		}
		if (options.shouldDumpOnExit()) {
			dumpReport();
		}

		try {
			deleteDirectoryIfEmpty(options.getOutputDirectory());
		} catch (IOException e) {
			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.",
					options.getOutputDirectory(), e);
		}
	}

	/**
	 * Delete a directory from disk if it is empty. This method does nothing if the path provided does not exist or
	 * point to a file.
	 *
	 * @throws IOException if the deletion of the directory fails
	 */
	private static void deleteDirectoryIfEmpty(Path directory) throws IOException {
		if (!Files.isDirectory(directory)) {
			return;
		}

		try (Stream 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.convert(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);
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy