ReportAggregateMojo.java

/*******************************************************************************
 * Copyright (c) 2009, 2024 Mountainminds GmbH & Co. KG and Contributors
 * This program and the accompanying materials are made available under
 * the terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *    John Oliver, Marc R. Hoffmann, Jan Wloka - initial API and implementation
 *
 *******************************************************************************/
package org.jacoco.maven;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
import org.apache.maven.artifact.versioning.VersionRange;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.jacoco.report.IReportGroupVisitor;

/**
 * <p>
 * Creates a structured code coverage report (HTML, XML, and CSV) from multiple
 * projects within reactor. The report is created from all modules this project
 * depends on, and optionally this project itself. From those projects class and
 * source files as well as JaCoCo execution data files will be collected. In
 * addition execution data is collected from the project itself. This also
 * allows to create coverage reports when tests are in separate projects than
 * the code under test, for example in case of integration tests.
 * </p>
 *
 * <p>
 * Using the dependency scope allows to distinguish projects which contribute
 * execution data but should not become part of the report:
 * </p>
 *
 * <ul>
 * <li><code>compile</code>, <code>runtime</code>, <code>provided</code>:
 * Project source and execution data is included in the report.</li>
 * <li><code>test</code>: Only execution data is considered for the report.</li>
 * </ul>
 *
 * @since 0.7.7
 */
@Mojo(name = "report-aggregate", threadSafe = true)
public class ReportAggregateMojo extends AbstractReportMojo {

	/**
	 * A list of execution data files to include in the report from each
	 * project. May use wildcard characters (* and ?). When not specified all
	 * *.exec files from the target folder will be included.
	 */
	@Parameter
	List<String> dataFileIncludes;

	/**
	 * A list of execution data files to exclude from the report. May use
	 * wildcard characters (* and ?). When not specified nothing will be
	 * excluded.
	 */
	@Parameter
	List<String> dataFileExcludes;

	/**
	 * Output directory for the reports. Note that this parameter is only
	 * relevant if the goal is run from the command line or from the default
	 * build lifecycle. If the goal is run indirectly as part of a site
	 * generation, the output directory configured in the Maven Site Plugin is
	 * used instead.
	 */
	@Parameter(defaultValue = "${project.reporting.outputDirectory}/jacoco-aggregate")
	private File outputDirectory;

	/**
	 * Include this project in the report. If true then this projects class and
	 * source files as well as JaCoCo execution data files will be collected.
	 *
	 * @since 0.8.9
	 */
	@Parameter(defaultValue = "false")
	private boolean includeCurrentProject;

	/**
	 * The projects in the reactor.
	 */
	@Parameter(property = "reactorProjects", readonly = true)
	private List<MavenProject> reactorProjects;

	@Override
	boolean canGenerateReportRegardingDataFiles() {
		return true;
	}

	@Override
	boolean canGenerateReportRegardingClassesDirectory() {
		return true;
	}

	@Override
	void loadExecutionData(final ReportSupport support) throws IOException {
		// https://issues.apache.org/jira/browse/MNG-5440
		if (dataFileIncludes == null) {
			dataFileIncludes = Arrays.asList("target/*.exec");
		}

		final FileFilter filter = new FileFilter(dataFileIncludes,
				dataFileExcludes);
		loadExecutionData(support, filter, project.getBasedir());
		for (final MavenProject dependency : findDependencies(
				Artifact.SCOPE_COMPILE, Artifact.SCOPE_RUNTIME,
				Artifact.SCOPE_PROVIDED, Artifact.SCOPE_TEST)) {
			loadExecutionData(support, filter, dependency.getBasedir());
		}
	}

	private void loadExecutionData(final ReportSupport support,
			final FileFilter filter, final File basedir) throws IOException {
		for (final File execFile : filter.getFiles(basedir)) {
			support.loadExecutionData(execFile);
		}
	}

	@Override
	File getOutputDirectory() {
		return outputDirectory;
	}

	@Override
	void createReport(final IReportGroupVisitor visitor,
			final ReportSupport support) throws IOException {
		final IReportGroupVisitor group = visitor.visitGroup(title);
		if (includeCurrentProject) {
			processProject(support, group, project);
		}
		for (final MavenProject dependency : findDependencies(
				Artifact.SCOPE_COMPILE, Artifact.SCOPE_RUNTIME,
				Artifact.SCOPE_PROVIDED)) {
			processProject(support, group, dependency);
		}
	}

	private void processProject(final ReportSupport support,
			final IReportGroupVisitor group, final MavenProject project)
			throws IOException {
		support.processProject(group, project.getArtifactId(), project,
				getIncludes(), getExcludes(), sourceEncoding);
	}

	public File getReportOutputDirectory() {
		return outputDirectory;
	}

	public void setReportOutputDirectory(final File reportOutputDirectory) {
		if (reportOutputDirectory != null && !reportOutputDirectory
				.getAbsolutePath().endsWith("jacoco-aggregate")) {
			outputDirectory = new File(reportOutputDirectory,
					"jacoco-aggregate");
		} else {
			outputDirectory = reportOutputDirectory;
		}
	}

	public String getOutputName() {
		return "jacoco-aggregate/index";
	}

	public String getName(final Locale locale) {
		return "JaCoCo Aggregate";
	}

	private List<MavenProject> findDependencies(final String... scopes) {
		final List<MavenProject> result = new ArrayList<MavenProject>();
		final List<String> scopeList = Arrays.asList(scopes);
		for (final Object dependencyObject : project.getDependencies()) {
			final Dependency dependency = (Dependency) dependencyObject;
			if (scopeList.contains(dependency.getScope())) {
				final MavenProject project = findProjectFromReactor(dependency);
				if (project != null) {
					result.add(project);
				}
			}
		}
		return result;
	}

	/**
	 * Note that if dependency specified using version range and reactor
	 * contains multiple modules with same artifactId and groupId but of
	 * different versions, then first dependency which matches range will be
	 * selected. For example in case of range <code>[0,2]</code> if version 1 is
	 * before version 2 in reactor, then version 1 will be selected.
	 */
	private MavenProject findProjectFromReactor(final Dependency d) {
		final VersionRange depVersionAsRange;
		try {
			depVersionAsRange = VersionRange
					.createFromVersionSpec(d.getVersion());
		} catch (final InvalidVersionSpecificationException e) {
			throw new AssertionError(e);
		}

		for (final MavenProject p : reactorProjects) {
			final DefaultArtifactVersion pv = new DefaultArtifactVersion(
					p.getVersion());
			if (p.getGroupId().equals(d.getGroupId())
					&& p.getArtifactId().equals(d.getArtifactId())
					&& depVersionAsRange.containsVersion(pv)) {
				return p;
			}
		}
		return null;
	}

}