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

org.scoverage.plugin.SCoverageReportMojo Maven / Gradle / Ivy

There is a newer version: 2.0.5
Show newest version
/*
 * Copyright 2014-2019 Grzegorz Slowikowski (gslowikowski at gmail dot com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.scoverage.plugin;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;

import org.apache.maven.doxia.module.xhtml.decoration.render.RenderingContext;
import org.apache.maven.doxia.siterenderer.sink.SiteRendererSink;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Execute;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.reporting.MavenReport;
import org.apache.maven.reporting.MavenReportException;

import org.codehaus.plexus.util.StringUtils;

import scala.Option;
import scala.collection.JavaConverters;
import scala.collection.Seq;

import scoverage.Constants;
import scoverage.Coverage;
import scoverage.IOUtils;
import scoverage.Serializer;
import scoverage.report.CoberturaXmlWriter;
import scoverage.report.CoverageAggregator;
import scoverage.report.ScoverageHtmlWriter;
import scoverage.report.ScoverageXmlWriter;

/**
 * Generates code coverage by unit tests report in forked {@code scoverage} life cycle.
 * 
*
* In forked {@code scoverage} life cycle project is compiled with SCoverage instrumentation * and unit tests are executed before report generation. *
* * @author Grzegorz Slowikowski * @since 1.0.0 */ @Mojo( name = "report", threadSafe = false ) @Execute( lifecycle = "scoverage", phase = LifecyclePhase.TEST ) public class SCoverageReportMojo extends AbstractMojo implements MavenReport { /** * Allows SCoverage to be skipped. *
* * @since 1.0.0 */ @Parameter( property = "scoverage.skip", defaultValue = "false" ) private boolean skip; /** * In multi-module project additionally generate aggregated SCoverage report. *
* * @since 1.1.0 */ @Parameter( property = "scoverage.aggregate", defaultValue = "false" ) private boolean aggregate; /** * In multi-module project generate only aggregated SCoverage report. *
*
* Scoverage reports for individual modules will not be generated. *
* * @since 1.4.0 */ @Parameter( property = "scoverage.aggregateOnly", defaultValue = "false" ) private boolean aggregateOnly; /** * The file encoding to use when reading Scala sources. *
* * @since 1.2.0 */ @Parameter( property = "encoding", defaultValue = "${project.build.sourceEncoding}" ) private String encoding; /** * Specifies if the build will fail if there are errors during report execution or not. */ @Parameter( property = "scoverage.failOnError", defaultValue = "true", readonly = true ) private boolean failOnError; /** * Maven project to interact with. */ @Parameter( defaultValue = "${project}", readonly = true, required = true ) private MavenProject project; /** * All Maven projects in the reactor. */ @Parameter( defaultValue = "${reactorProjects}", required = true, readonly = true ) private List reactorProjects; /** * Destination directory where the coverage data files should be written. */ @Parameter( property = "scoverage.dataDirectory", defaultValue = "${project.build.directory}/scoverage-data", required = true, readonly = true ) private File dataDirectory; /** * Destination directory for generated HTML report files. */ @Parameter( property = "scoverage.outputDirectory", defaultValue = "${project.reporting.outputDirectory}/scoverage", required = true, readonly = true ) private File outputDirectory; /** * Destination directory for XML report files. */ @Parameter( property = "scoverage.xmlOutputDirectory", defaultValue = "${project.build.directory}", required = true, readonly = true ) private File xmlOutputDirectory; /** * The name of the destination directory. *
*/ @Parameter( property = "destDir", defaultValue = "scoverage", required = true, readonly = true ) private String destDir; /** * The name of the SCoverage report to be displayed in the Maven Generated Reports page * (i.e. project-reports.html). */ @Parameter( property = "name", readonly = true ) private String name; /** * The description of the Scoverage report to be displayed in the Maven Generated Reports page * (i.e. project-reports.html). */ @Parameter( property = "description", readonly = true ) private String description; /** {@inheritDoc} */ @Override public String getName( Locale locale ) { if ( StringUtils.isEmpty( name ) ) { return getBundle( locale ).getString( "report.scoverage.name" ); } return name; } /** {@inheritDoc} */ @Override public String getDescription( Locale locale ) { if ( StringUtils.isEmpty( description ) ) { return getBundle( locale ).getString( "report.scoverage.description" ); } return description; } /** {@inheritDoc} */ @Override @SuppressWarnings( "deprecation" ) public void generate( org.codehaus.doxia.sink.Sink sink, Locale locale ) throws MavenReportException { boolean canGenerateNonAggregatedReport = canGenerateNonAggregatedReport(); boolean canGenerateAggregatedReport = canGenerateAggregatedReport(); boolean canAttachAggregatedReportToSite = canAttachAggregatedReportToSite(); if ( canAttachAggregatedReportToSite && !( canGenerateNonAggregatedReport || canGenerateAggregatedReport ) ) { return; // aggregated report for top level project is generated by last reactor project } try { long ts = System.currentTimeMillis(); // If top-level project is last reactor project it should generate ONLY aggregated report here if ( canGenerateNonAggregatedReport ) { generateReports(); } // Aggregated report must be generated in last reactor project. It may be top-level // project (this is very rare case) or any other project. // Whatever project it is, it must generate report in top-level project's site directory. // WARNING: Last reactor project cannot have scoverage generation skipped // ('skip' configuration parameter set)! if ( canGenerateAggregatedReport ) { generateAggregatedReports(); } long te = System.currentTimeMillis(); getLog().debug( String.format( "Mojo execution time: %d ms", te - ts ) ); } catch ( MavenReportException e ) { if ( failOnError ) { throw e; } getLog().error( "Error while creating scoverage report: " + e.getMessage(), e ); } catch ( RuntimeException e ) { if ( failOnError ) { throw new MavenReportException( "Report generation exception", e ); } getLog().error( "Error while creating scoverage report: " + e.getMessage(), e ); } } /** {@inheritDoc} */ @Override public String getOutputName() { return destDir + "/index"; } /** {@inheritDoc} */ @Override public boolean isExternalReport() { return true; } /** {@inheritDoc} */ @Override public boolean canGenerateReport() { if ( !skip && !"pom".equals( project.getPackaging() ) ) { SCoverageForkedLifecycleConfigurator.afterForkedLifecycleExit( project, reactorProjects ); } boolean canGenerateNonAggregatedReport = canGenerateNonAggregatedReport(); boolean canAttachAggregatedReportToSite = canAttachAggregatedReportToSite(); boolean result = canGenerateNonAggregatedReport || canAttachAggregatedReportToSite; if ( !result && canGenerateAggregatedReport() ) { // last project, but not top-level one // generate here, because 'false' must be returned to Maven in order to avoid adding to this module's site try { generateAggregatedReports(); } catch ( MavenReportException e ) { throw new RuntimeException( e ); } } return result; } private boolean canGenerateNonAggregatedReport() { if ( skip ) { return false; } if ( "pom".equals( project.getPackaging() ) ) { return false; } if ( aggregateOnly && reactorProjects.size() > 1 ) { return false; } File coverageFile = Serializer.coverageFile( dataDirectory ); if ( !coverageFile.exists() || !coverageFile.isFile() ) { return false; } return true; } private boolean canGenerateAggregatedReport() { return ( aggregate || aggregateOnly ) && reactorProjects.size() > 1 && project == reactorProjects.get( reactorProjects.size() - 1 ); } private boolean canAttachAggregatedReportToSite() { return ( aggregate || aggregateOnly ) && reactorProjects.size() > 1 && project.isExecutionRoot(); } /** {@inheritDoc} */ @Override public String getCategoryName() { return MavenReport.CATEGORY_PROJECT_REPORTS; } /** {@inheritDoc} */ @Override public File getReportOutputDirectory() { return outputDirectory; } /** {@inheritDoc} */ @Override public void setReportOutputDirectory( File reportOutputDirectory ) { updateReportOutputDirectory( reportOutputDirectory ); } private void updateReportOutputDirectory( File reportOutputDirectory ) { if ( reportOutputDirectory != null && destDir != null && !reportOutputDirectory.getAbsolutePath().endsWith( destDir ) ) { this.outputDirectory = new File( reportOutputDirectory, destDir ); } else { this.outputDirectory = reportOutputDirectory; } } /** * Generates SCoverage report. * * @throws MojoExecutionException if unexpected problem occurs */ @Override public void execute() throws MojoExecutionException { if ( !canGenerateReport() ) { getLog().info( "Skipping SCoverage report generation" ); return; } try { RenderingContext context = new RenderingContext( outputDirectory, getOutputName() + ".html" ); SiteRendererSink sink = new SiteRendererSink( context ); Locale locale = Locale.getDefault(); generate( sink, locale ); } catch ( MavenReportException e ) { String prefix = "An error has occurred in " + getName( Locale.ENGLISH ) + " report generation"; throw new MojoExecutionException( prefix + ": " + e.getMessage(), e ); } } /** * Gets the resource bundle for the specified locale. * * @param locale The locale of the currently generated report. * @return The resource bundle for the requested locale. */ private ResourceBundle getBundle( Locale locale ) { return ResourceBundle.getBundle( "scoverage-report", locale, getClass().getClassLoader() ); } private void generateReports() throws MavenReportException /*, RuntimeException*/ { @SuppressWarnings( "unchecked" ) List sourceDirs = project.getExecutionProject().getCompileSourceRoots(); List sourceRoots = new ArrayList( sourceDirs.size() ); for ( String dir: sourceDirs ) { sourceRoots.add( new File( dir ) ); } mkdirs( outputDirectory ); mkdirs( xmlOutputDirectory ); File coverageFile = Serializer.coverageFile( dataDirectory ); getLog().info( String.format( "Reading scoverage instrumentation [%s]...", coverageFile.getAbsolutePath() ) ); Coverage coverage = Serializer.deserialize( coverageFile ); getLog().info( String.format( "Reading scoverage measurements [%s*]...", new File( dataDirectory, Constants.MeasurementsPrefix() ).getAbsolutePath() ) ); List measurementFiles = Arrays.asList( IOUtils.findMeasurementFiles( dataDirectory ) ); scala.collection.Set measurements = IOUtils.invoked( JavaConverters.asScalaBuffer( measurementFiles ) ); coverage.apply( measurements ); getLog().info( "Generating coverage reports..." ); writeReports( coverage, sourceRoots, xmlOutputDirectory, xmlOutputDirectory, outputDirectory ); getLog().info( "Coverage reports completed." ); } private void generateAggregatedReports() throws MavenReportException { List scoverageDataDirs = new ArrayList(); List sourceRoots = new ArrayList(); MavenProject topLevelModule = null; for ( MavenProject module : reactorProjects ) { if ( module.isExecutionRoot() ) { topLevelModule = module; } else if ( !module.getPackaging().equals( "pom" ) ) { File scoverageDataDir = rebase( dataDirectory, module ); if ( scoverageDataDir.isDirectory() ) { scoverageDataDirs.add( scoverageDataDir ); File sourceRootsFile = new File( scoverageDataDir, "source.roots" ); if ( sourceRootsFile.isFile() ) { try { BufferedReader r = new BufferedReader( new InputStreamReader( new FileInputStream( sourceRootsFile ), "UTF-8" ) ); try { String path = r.readLine(); while ( path != null ) { sourceRoots.add( new File( path ) ); path = r.readLine(); } } finally { r.close(); } } catch ( IOException e ) { throw new MavenReportException( "...", e ); } } } } } /* Empty report must be generated or top-level site will contain invalid link to non-existent Scoverage report if ( scoverageDataDirs.isEmpty() ) { getLog().info( "No subproject data to aggregate, skipping SCoverage report generation" ); return; }*/ if ( getLog().isDebugEnabled() && scoverageDataDirs.size() > 0 ) { getLog().debug( String.format( "Found %d subproject subproject scoverage data directories:", scoverageDataDirs.size() ) ); for ( File dataDir: scoverageDataDirs ) { getLog().debug( String.format( "- %s", dataDir.getAbsolutePath() ) ); } } else { getLog().info( String.format( "Found %d subproject scoverage data directories.", scoverageDataDirs.size() ) ); } File topLevelModuleOutputDirectory = rebase( outputDirectory, topLevelModule ); File topLevelModuleXmlOutputDirectory = rebase( xmlOutputDirectory, topLevelModule ); mkdirs( topLevelModuleOutputDirectory ); mkdirs( topLevelModuleXmlOutputDirectory ); Coverage coverage = CoverageAggregator.aggregatedCoverage( JavaConverters.asScalaBuffer( scoverageDataDirs ).toSeq() ); getLog().info( "Generating coverage aggregated reports..." ); writeReports( coverage, sourceRoots, topLevelModuleXmlOutputDirectory, topLevelModuleXmlOutputDirectory, topLevelModuleOutputDirectory ); getLog().info( "Coverage aggregated reports completed." ); } private void writeReports( Coverage coverage, List sourceRoots, File coberturaXmlOutputDirectory, File scoverageXmlOutputDirectory, File scoverageHtmlOutputDirectory ) { Seq sourceRootsAsScalaSeq = JavaConverters.asScalaBuffer( sourceRoots ); new CoberturaXmlWriter( sourceRootsAsScalaSeq, coberturaXmlOutputDirectory ).write( coverage ); getLog().info( String.format( "Written Cobertura XML report [%s]", new File( coberturaXmlOutputDirectory, "cobertura.xml" ).getAbsolutePath() ) ); new ScoverageXmlWriter( sourceRootsAsScalaSeq, scoverageXmlOutputDirectory, false ).write( coverage ); getLog().info( String.format( "Written XML coverage report [%s]", new File( scoverageXmlOutputDirectory, "scoverage.xml" ).getAbsolutePath() ) ); new ScoverageHtmlWriter( sourceRootsAsScalaSeq, scoverageHtmlOutputDirectory, Option.apply( encoding ) ).write( coverage ); getLog().info( String.format( "Written HTML coverage report [%s]", new File( scoverageHtmlOutputDirectory, "index.html" ).getAbsolutePath() ) ); getLog().info( String.format( "Statement coverage.: %s%%", coverage.statementCoverageFormatted() ) ); getLog().info( String.format( "Branch coverage....: %s%%", coverage.branchCoverageFormatted() ) ); } private void mkdirs( File directory ) throws MavenReportException { if ( !directory.exists() && !directory.mkdirs() ) { throw new MavenReportException( String.format( "Cannot create \"%s\" directory ", directory.getAbsolutePath() ) ); } else if ( directory.exists() && !directory.isDirectory() ) { throw new MavenReportException( String.format( "Directory \"%s\" exists but is not a directory ", directory.getAbsolutePath() ) ); } } private File rebase( File file, MavenProject otherModule ) { return new File( file.getAbsolutePath().replace( project.getBasedir().getAbsolutePath(), otherModule.getBasedir().getAbsolutePath() ) ); } }