package org.codehaus.mojo.emma;
/*
* The MIT License
*
* Copyright (c) 2007-8, The Codehaus
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import nu.xom.Builder;
import nu.xom.Document;
import nu.xom.Element;
import nu.xom.Nodes;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.codehaus.mojo.emma.task.ReportTask;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.SelectorUtils;
import org.codehaus.plexus.util.StringUtils;
/**
* Check last intrumentation results.
*
* @author <a href="mailto:alexandre.roman@gmail.com">Alexandre ROMAN</a>
* @goal check
* @execute phase="test" lifecycle="emma"
* @phase verify
*/
public class EmmaCheckMojo
extends AbstractEmmaMojo
{
/**
* The tag types.
*/
private static final Map TAG_2_TYPE = new HashMap();
static
{
TAG_2_TYPE.put( "all", CoverageResult.Type.ALL );
TAG_2_TYPE.put( "package", CoverageResult.Type.PACKAGE );
TAG_2_TYPE.put( "class", CoverageResult.Type.CLASS );
TAG_2_TYPE.put( "method", CoverageResult.Type.METHOD );
}
/**
* Location to XML coverage file.
*
* @parameter expression="${emma.coverageFile}"
* default-value="${project.reporting.outputDirectory}/emma/coverage.xml"
* @required
*/
protected File coverageFile;
/**
* Check configuration.
*
* @parameter
*/
protected CheckConfiguration check;
/**
* Location to store class coverage metadata.
*
* @parameter expression="${emma.metadataFile}" default-value="${project.build.directory}/coverage.em"
* @required
*/
protected File metadataFile;
/**
* Class coverage data files.
*
* @parameter
*/
protected File[] dataFiles;
/**
* Location to store EMMA generated resources.
*
* @parameter default-value="${project.reporting.outputDirectory}/emma"
* @required
*/
protected File outputDirectory;
/**
* Does the work.
*
* @throws MojoExecutionException if things go wrong.
* @throws MojoFailureException if things go wrong.
*/
protected void doExecute()
throws MojoExecutionException, MojoFailureException
{
if ( dataFiles == null )
{
dataFiles = new File[] { new File( project.getBasedir(), "coverage.ec" ) };
}
dataFiles = EmmaUtils.fixDataFileLocations( project, dataFiles );
if ( coverageFile == null || !coverageFile.exists() )
{
if ( dataFiles != null && dataFiles.length > 0 )
{
// XML report was not generated: let's generate it now!
coverageFile = generateReport();
}
else
{
getLog().info( "Not checking EMMA coverage results, as no results were found" );
return;
}
}
if ( check == null )
{
getLog().info( "Not checking EMMA coverage results, as no configuration was set" );
return;
}
getLog().info( "Checking EMMA coverage results" );
// read XML coverage results
final Document doc;
InputStream input = null;
try
{
input = new FileInputStream( coverageFile );
final Builder builder = new Builder();
doc = builder.build( input );
}
catch ( Exception e )
{
throw new MojoExecutionException( "Failed to read EMMA coverage results from file: "
+ coverageFile.getPath(), e );
}
finally
{
IOUtil.close( input );
}
CoverageResult allResults = null;
final List results = new ArrayList( 3 );
// parse XML coverage results
try
{
final Nodes allNodes = doc.query( "/report/data/all" );
if ( allNodes.size() > 0 )
{
allResults = toCoverageResult( (Element) allNodes.get( 0 ) );
}
final String[] expressions =
{ "/report/data/all/package", "/report/data/all/package/srcfile/class",
"/report/data/all/package/srcfile/class/method" };
for ( int i = 0; i < expressions.length; ++i )
{
final Nodes nodes = doc.query( expressions[i] );
final int nodesLen = nodes.size();
for ( int j = 0; j < nodesLen; ++j )
{
final CoverageResult r = toCoverageResult( (Element) nodes.get( j ) );
if ( r != null )
{
results.add( r );
}
}
}
}
catch ( Exception e )
{
throw new MojoExecutionException( "Failed to parse EMMA coverage results from file: "
+ coverageFile.getName(), e );
}
if ( getLog().isDebugEnabled() )
{
if ( allResults == null && results.isEmpty() )
{
getLog().debug( "No coverage results!" );
}
else
{
getLog().debug( "Coverage results:" );
if ( allResults != null )
{
getLog().debug( " o " + allResults );
}
// sort results for easier reading
Collections.sort( results, new CoverageResultComparator() );
for ( final Iterator i = results.iterator(); i.hasNext(); )
{
final CoverageResult r = (CoverageResult) i.next();
getLog().debug( " o " + r );
}
}
}
// check coverage results
checkResults( allResults, results );
}
/**
* Checks the results against the targets.
*
* @param allResults The overall results.
* @param results The results based on regex limits.
* @throws MojoExecutionException if something goes wrong.
*/
private void checkResults( CoverageResult allResults, List results )
throws MojoFailureException
{
if ( allResults != null )
{
if ( allResults.getBlockRate() != CoverageResult.UNKNOWN_RATE
&& allResults.getBlockRate() < check.getBlockRate() )
{
getLog().warn(
"Insufficient code coverage for blocks: " + allResults.getBlockRate() + "% < "
+ check.getBlockRate() + "%" );
fail();
return;
}
else if ( allResults.getClassRate() != CoverageResult.UNKNOWN_RATE
&& allResults.getClassRate() < check.getClassRate() )
{
getLog().warn(
"Insufficient code coverage for classes: " + allResults.getClassRate() + "% < "
+ check.getClassRate() + "%" );
fail();
return;
}
else if ( allResults.getMethodRate() != CoverageResult.UNKNOWN_RATE
&& allResults.getMethodRate() < check.getMethodRate() )
{
getLog().warn(
"Insufficient code coverage for methods: " + allResults.getMethodRate() + "% < "
+ check.getMethodRate() + "%" );
fail();
return;
}
else if ( allResults.getLineRate() != CoverageResult.UNKNOWN_RATE
&& allResults.getLineRate() < check.getLineRate() )
{
getLog().warn(
"Insufficient code coverage for lines: " + allResults.getLineRate() + "% < "
+ check.getLineRate() + "%" );
fail();
return;
}
}
for ( int i = 0; i < check.getRegexes().length; ++i )
{
final CheckConfiguration.Regex regex = check.getRegexes()[i];
for ( final Iterator j = results.iterator(); j.hasNext(); )
{
final CoverageResult result = (CoverageResult) j.next();
if ( !SelectorUtils.match( regex.getPattern(), result.getName() ) )
{
continue;
}
if ( result.getBlockRate() != CoverageResult.UNKNOWN_RATE
&& result.getBlockRate() < regex.getBlockRate() )
{
getLog().warn(
"Insufficient code coverage for blocks in " + regex.getPattern() + ": "
+ result.getBlockRate() + "% < " + regex.getBlockRate() + "%" );
fail();
return;
}
else if ( result.getClassRate() != CoverageResult.UNKNOWN_RATE
&& result.getClassRate() < regex.getClassRate() )
{
getLog().warn(
"Insufficient code coverage for classes in " + regex.getPattern() + ": "
+ result.getClassRate() + "% < " + regex.getClassRate() + "%" );
fail();
return;
}
else if ( result.getMethodRate() != CoverageResult.UNKNOWN_RATE
&& result.getMethodRate() < regex.getMethodRate() )
{
getLog().warn(
"Insufficient code coverage for methods in " + regex.getPattern() + ": "
+ result.getMethodRate() + "% < " + regex.getMethodRate() + "%" );
fail();
return;
}
else if ( result.getLineRate() != CoverageResult.UNKNOWN_RATE
&& result.getLineRate() < regex.getLineRate() )
{
getLog().warn(
"Insufficient code coverage for lines in " + regex.getPattern() + ": "
+ result.getLineRate() + "% < " + regex.getLineRate() + "%" );
fail();
return;
}
}
}
getLog().info( "EMMA coverage results are valid" );
}
/**
* Fail Mojo execution if coverage results are not valid.
*
* @throws MojoFailureException if the results are not valid
*/
private void fail()
throws MojoFailureException
{
final String failMsg = "Failed to validate EMMA coverage results: see report for more information";
if ( check.isHaltOnFailure() )
{
throw new MojoFailureException( failMsg );
}
else
{
getLog().warn( failMsg );
}
}
/**
* Convert XML element to {@link CoverageResult} instance.
*
* @param elem The element to convert.
* @return The {@link CoverageResult}.
*/
private CoverageResult toCoverageResult( Element elem )
{
final CoverageResult.Type type = (CoverageResult.Type) TAG_2_TYPE.get( elem.getLocalName() );
if ( type == null )
{
return null;
}
final CoverageResult result;
if ( CoverageResult.Type.ALL.equals( type ) )
{
result = new CoverageResult();
}
else
{
final String name;
if ( CoverageResult.Type.CLASS.equals( type ) )
{
name = fullClassName( elem );
}
else if ( CoverageResult.Type.METHOD.equals( type ) )
{
name = fullMethodName( elem );
}
else
{
name = elem.getAttributeValue( "name" );
}
result = new CoverageResult( type, name );
}
final Nodes coverageNodes = elem.query( "coverage" );
final int len = coverageNodes.size();
for ( int i = 0; i < len; ++i )
{
final Element coverageElem = (Element) coverageNodes.get( i );
final String coverageType = coverageElem.getAttributeValue( "type" );
if ( StringUtils.isEmpty( coverageType ) )
{
continue;
}
final String coverageValueStr = coverageElem.getAttributeValue( "value" );
if ( StringUtils.isEmpty( coverageValueStr ) )
{
continue;
}
final int percentIndex = coverageValueStr.indexOf( '%' );
if ( percentIndex == -1 )
{
continue;
}
final int coverageValue;
try
{
coverageValue = Integer.parseInt( coverageValueStr.substring( 0, percentIndex ).trim() );
}
catch ( NumberFormatException e )
{
getLog().debug( "Failed to parse coverage value: " + coverageValueStr, e );
continue;
}
if ( coverageType.startsWith( "class" ) )
{
result.setClassRate( coverageValue );
}
else if ( coverageType.startsWith( "method" ) )
{
result.setMethodRate( coverageValue );
}
else if ( coverageType.startsWith( "block" ) )
{
result.setBlockRate( coverageValue );
}
else if ( coverageType.startsWith( "line" ) )
{
result.setLineRate( coverageValue );
}
}
return result;
}
/**
* Get full class name (package + class) for "class" XML element.
*
* @param elem The element.
* @return the full class name (package + class) for "class" XML element.
*/
private String fullClassName( Element elem )
{
final Element packageElem = (Element) elem.getParent().getParent();
final String packageName = packageElem.getAttributeValue( "name" );
final String className = elem.getAttributeValue( "name" );
return packageName.length() != 0 ? packageName + "." + className : className;
}
/**
* Get full method name (package + class + method) for "method" XML element.
*
* @param elem The element.
* @return the full method name (package + class + method) for "method" XML element.
*/
private String fullMethodName( Element elem )
{
final Element classElem = (Element) elem.getParent();
final String name = elem.getAttributeValue( "name" );
final int i = name.indexOf( " (" );
final String methodName;
if ( i != -1 )
{
methodName = name.substring( 0, i );
}
else
{
// no parenthesis found: must be a static block
methodName = "static";
}
return fullClassName( classElem ) + "." + methodName;
}
/**
* Generate the report.
*
* @return the report file.
* @throws MojoExecutionException if things go wrong.
*/
private File generateReport()
throws MojoExecutionException
{
final ReportTask task = new ReportTask();
task.setVerbose( verbose );
task.setMetadataFile( metadataFile );
task.setDataFiles( dataFiles );
task.setOutputDirectory( outputDirectory );
task.setGenerateOnlyXml( true );
try
{
task.execute();
}
catch ( IOException e )
{
throw new MojoExecutionException( e.getMessage(), e );
}
return new File( outputDirectory, "coverage.xml" );
}
/**
* Sort {@link CoverageResult} instance against their name.
*/
private static class CoverageResultComparator
implements Comparator
{
/**
* Compares two parameters.
*
* @param o1 the first parameter.
* @param o2 the second parameter.
* @return < 0 if o1 < o2; == 0 if o1 == o2; > 0 if o1 > o2
*/
public int compare( Object o1, Object o2 )
{
final CoverageResult r1 = (CoverageResult) o1;
final CoverageResult r2 = (CoverageResult) o2;
return r1.getName().compareTo( r2.getName() );
}
}
}