ReportTask.java

    1/*******************************************************************************
    2 * Copyright (c) 2009, 2010 Mountainminds GmbH & Co. KG and Contributors
    3 * All rights reserved. This program and the accompanying materials
    4 * are made available under the terms of the Eclipse Public License v1.0
    5 * which accompanies this distribution, and is available at
    6 * http://www.eclipse.org/legal/epl-v10.html
    7 *
    8 * Contributors:
    9 *    Marc R. Hoffmann - initial API and implementation
   10 *    
   11 *******************************************************************************/
   12package org.jacoco.ant;
   13
   14import static java.lang.String.format;
   15
   16import java.io.BufferedInputStream;
   17import java.io.File;
   18import java.io.FileOutputStream;
   19import java.io.IOException;
   20import java.io.InputStream;
   21import java.io.InputStreamReader;
   22import java.io.Reader;
   23import java.util.ArrayList;
   24import java.util.Collection;
   25import java.util.HashMap;
   26import java.util.Iterator;
   27import java.util.List;
   28import java.util.Locale;
   29import java.util.Map;
   30import java.util.zip.ZipOutputStream;
   31
   32import org.apache.tools.ant.BuildException;
   33import org.apache.tools.ant.Project;
   34import org.apache.tools.ant.Task;
   35import org.apache.tools.ant.types.Resource;
   36import org.apache.tools.ant.types.resources.FileResource;
   37import org.apache.tools.ant.types.resources.Union;
   38import org.apache.tools.ant.util.FileUtils;
   39import org.jacoco.core.analysis.BundleCoverage;
   40import org.jacoco.core.analysis.ClassCoverage;
   41import org.jacoco.core.analysis.CoverageBuilder;
   42import org.jacoco.core.analysis.CoverageNodeImpl;
   43import org.jacoco.core.analysis.ICoverageNode;
   44import org.jacoco.core.analysis.ICoverageNode.ElementType;
   45import org.jacoco.core.analysis.PackageCoverage;
   46import org.jacoco.core.data.ExecutionDataReader;
   47import org.jacoco.core.data.ExecutionDataStore;
   48import org.jacoco.core.data.SessionInfoStore;
   49import org.jacoco.core.instr.Analyzer;
   50import org.jacoco.report.FileMultiReportOutput;
   51import org.jacoco.report.FileSingleReportOutput;
   52import org.jacoco.report.IMultiReportOutput;
   53import org.jacoco.report.IReportFormatter;
   54import org.jacoco.report.IReportVisitor;
   55import org.jacoco.report.ISourceFileLocator;
   56import org.jacoco.report.MultiFormatter;
   57import org.jacoco.report.ZipMultiReportOutput;
   58import org.jacoco.report.csv.CSVFormatter;
   59import org.jacoco.report.html.HTMLFormatter;
   60import org.jacoco.report.xml.XMLFormatter;
   61
   62/**
   63 * Task for coverage report generation. Experimental implementation that needs
   64 * refinement.
   65 * 
   66 * @author Marc R. Hoffmann
   67 * @version 0.4.1.20101007204400
   68 */
   69public class ReportTask extends Task {
   70
   71    /**
   72     * The source files are specified in a resource collection with additional
   73     * attributes.
   74     */
   75    public static class SourceFilesElement extends Union {
   76
   77        String encoding;
   78
   79        /**
   80         * Defines the optional source file encoding. If not set the platform
   81         * default is used.
   82         * 
   83         * @param encoding
   84         *            source file encoding
   85         */
   86        public void setEncoding(final String encoding) {
   87            this.encoding = encoding;
   88        }
   89
   90    }
   91
   92    /**
   93     * Container element for class file groups.
   94     */
   95    public static class GroupElement {
   96
   97        private final List<GroupElement> children = new ArrayList<GroupElement>();
   98
   99        private final Union classfiles = new Union();
  100
  101        private final SourceFilesElement sourcefiles = new SourceFilesElement();
  102
  103        private String name;
  104
  105        /**
  106         * Sets the name of the group.
  107         * 
  108         * @param name
  109         *            name of the group
  110         */
  111        public void setName(final String name) {
  112            this.name = name;
  113        }
  114
  115        /**
  116         * Creates a new child group.
  117         * 
  118         * @return new child group
  119         */
  120        public GroupElement createGroup() {
  121            final GroupElement group = new GroupElement();
  122            children.add(group);
  123            return group;
  124        }
  125
  126        /**
  127         * Returns the nested resource collection for class files.
  128         * 
  129         * @return resource collection for class files
  130         */
  131        public Union createClassfiles() {
  132            return classfiles;
  133        }
  134
  135        /**
  136         * Returns the nested resource collection for source files.
  137         * 
  138         * @return resource collection for source files
  139         */
  140        public SourceFilesElement createSourcefiles() {
  141            return sourcefiles;
  142        }
  143
  144    }
  145
  146    /**
  147     * Interface for child elements that define formatters.
  148     */
  149    private interface IFormatterElement {
  150
  151        IReportFormatter createFormatter() throws IOException;
  152
  153        void finish() throws IOException;
  154
  155    }
  156
  157    /**
  158     * Formatter Element for HTML reports.
  159     */
  160    public static class HTMLFormatterElement implements IFormatterElement {
  161
  162        private File destdir;
  163
  164        private File destfile;
  165
  166        private String footer = "";
  167
  168        private String encoding = "UTF-8";
  169
  170        private Locale locale = Locale.getDefault();
  171
  172        private ZipOutputStream zipOutput;
  173
  174        /**
  175         * Sets the output directory for the report.
  176         * 
  177         * @param destdir
  178         *            output directory
  179         */
  180        public void setDestdir(final File destdir) {
  181            this.destdir = destdir;
  182        }
  183
  184        /**
  185         * Sets the Zip output file for the report.
  186         * 
  187         * @param destfile
  188         *            Zip output file
  189         */
  190        public void setDestfile(final File destfile) {
  191            this.destfile = destfile;
  192        }
  193
  194        /**
  195         * Sets an optional footer text that will be displayed on every report
  196         * page.
  197         * 
  198         * @param text
  199         *            footer text
  200         */
  201        public void setFooter(final String text) {
  202            this.footer = text;
  203        }
  204
  205        /**
  206         * Sets the output encoding for generated HTML files. Default is UTF-8.
  207         * 
  208         * @param encoding
  209         *            output encoding
  210         */
  211        public void setEncoding(final String encoding) {
  212            this.encoding = encoding;
  213        }
  214
  215        /**
  216         * Sets the locale for generated text output. By default the platform
  217         * locale is used.
  218         * 
  219         * @param locale
  220         *            text locale
  221         */
  222        public void setLocale(final Locale locale) {
  223            this.locale = locale;
  224        }
  225
  226        public IReportFormatter createFormatter() throws IOException {
  227            final IMultiReportOutput output;
  228            if (destfile != null) {
  229                if (destdir != null) {
  230                    throw new BuildException(
  231                            "Either destination directory or file must be supplied, not both");
  232                }
  233                zipOutput = new ZipOutputStream(new FileOutputStream(destfile));
  234                output = new ZipMultiReportOutput(zipOutput);
  235
  236            } else {
  237                if (destdir == null) {
  238                    throw new BuildException(
  239                            "Destination directory or file must be supplied for html report");
  240                }
  241                output = new FileMultiReportOutput(destdir);
  242            }
  243            final HTMLFormatter formatter = new HTMLFormatter();
  244            formatter.setReportOutput(output);
  245            formatter.setFooterText(footer);
  246            formatter.setOutputEncoding(encoding);
  247            formatter.setLocale(locale);
  248            return formatter;
  249        }
  250
  251        public void finish() throws IOException {
  252            if (zipOutput != null) {
  253                zipOutput.close();
  254            }
  255        }
  256
  257    }
  258
  259    /**
  260     * Formatter Element for CSV reports.
  261     */
  262    public static class CSVFormatterElement implements IFormatterElement {
  263
  264        private File destfile;
  265
  266        private String encoding = "UTF-8";
  267
  268        /**
  269         * Sets the output file for the report.
  270         * 
  271         * @param destfile
  272         *            output file
  273         */
  274        public void setDestfile(final File destfile) {
  275            this.destfile = destfile;
  276        }
  277
  278        public IReportFormatter createFormatter() {
  279            if (destfile == null) {
  280                throw new BuildException(
  281                        "Destination file must be supplied for csv report");
  282            }
  283            final CSVFormatter formatter = new CSVFormatter();
  284            formatter.setReportOutput(new FileSingleReportOutput(destfile));
  285            formatter.setOutputEncoding(encoding);
  286            return formatter;
  287        }
  288
  289        /**
  290         * Sets the output encoding for generated XML file. Default is UTF-8.
  291         * 
  292         * @param encoding
  293         *            output encoding
  294         */
  295        public void setEncoding(final String encoding) {
  296            this.encoding = encoding;
  297        }
  298
  299        public void finish() {
  300        }
  301
  302    }
  303
  304    /**
  305     * Formatter Element for XML reports.
  306     */
  307    public static class XMLFormatterElement implements IFormatterElement {
  308
  309        private File destfile;
  310
  311        private String encoding = "UTF-8";
  312
  313        /**
  314         * Sets the output file for the report.
  315         * 
  316         * @param destfile
  317         *            output file
  318         */
  319        public void setDestfile(final File destfile) {
  320            this.destfile = destfile;
  321        }
  322
  323        /**
  324         * Sets the output encoding for generated XML file. Default is UTF-8.
  325         * 
  326         * @param encoding
  327         *            output encoding
  328         */
  329        public void setEncoding(final String encoding) {
  330            this.encoding = encoding;
  331        }
  332
  333        public IReportFormatter createFormatter() {
  334            if (destfile == null) {
  335                throw new BuildException(
  336                        "Destination file must be supplied for xml report");
  337            }
  338            final XMLFormatter formatter = new XMLFormatter();
  339            formatter.setReportOutput(new FileSingleReportOutput(destfile));
  340            formatter.setOutputEncoding(encoding);
  341            return formatter;
  342        }
  343
  344        public void finish() {
  345        }
  346
  347    }
  348
  349    private final Union executiondataElement = new Union();
  350
  351    private SessionInfoStore sessionInfoStore;
  352
  353    private ExecutionDataStore executionDataStore;
  354
  355    private final GroupElement structure = new GroupElement();
  356
  357    private final List<IFormatterElement> formatters = new ArrayList<IFormatterElement>();
  358
  359    /**
  360     * Returns the nested resource collection for execution data files.
  361     * 
  362     * @return resource collection for execution files
  363     */
  364    public Union createExecutiondata() {
  365        return executiondataElement;
  366    }
  367
  368    /**
  369     * Returns the root group element that defines the report structure.
  370     * 
  371     * @return root group element
  372     */
  373    public GroupElement createStructure() {
  374        return structure;
  375    }
  376
  377    /**
  378     * Creates a new HTML report element.
  379     * 
  380     * @return HTML report element
  381     */
  382    public HTMLFormatterElement createHtml() {
  383        final HTMLFormatterElement element = new HTMLFormatterElement();
  384        formatters.add(element);
  385        return element;
  386    }
  387
  388    /**
  389     * Creates a new CSV report element.
  390     * 
  391     * @return CSV report element
  392     */
  393    public CSVFormatterElement createCsv() {
  394        final CSVFormatterElement element = new CSVFormatterElement();
  395        formatters.add(element);
  396        return element;
  397    }
  398
  399    /**
  400     * Creates a new XML report element.
  401     * 
  402     * @return CSV report element
  403     */
  404    public XMLFormatterElement createXml() {
  405        final XMLFormatterElement element = new XMLFormatterElement();
  406        formatters.add(element);
  407        return element;
  408    }
  409
  410    @Override
  411    public void execute() throws BuildException {
  412        loadExecutionData();
  413        try {
  414            final IReportFormatter formatter = createFormatter();
  415            createReport(formatter);
  416            finishFormatters();
  417        } catch (final IOException e) {
  418            throw new BuildException("Error while creating report.", e);
  419        }
  420    }
  421
  422    private void loadExecutionData() {
  423        sessionInfoStore = new SessionInfoStore();
  424        executionDataStore = new ExecutionDataStore();
  425        for (final Iterator<?> i = executiondataElement.iterator(); i.hasNext();) {
  426            final Resource resource = (Resource) i.next();
  427            InputStream in = null;
  428            try {
  429                in = new BufferedInputStream(resource.getInputStream());
  430                final ExecutionDataReader reader = new ExecutionDataReader(in);
  431                reader.setSessionInfoVisitor(sessionInfoStore);
  432                reader.setExecutionDataVisitor(executionDataStore);
  433                reader.read();
  434            } catch (final IOException e) {
  435                throw new BuildException("Unable to read execution data file "
  436                        + resource.getName(), e);
  437            } finally {
  438                FileUtils.close(in);
  439            }
  440        }
  441    }
  442
  443    private IReportFormatter createFormatter() throws IOException {
  444        final MultiFormatter multi = new MultiFormatter();
  445        for (final IFormatterElement f : formatters) {
  446            multi.add(f.createFormatter());
  447        }
  448        return multi;
  449    }
  450
  451    private void finishFormatters() throws IOException {
  452        for (final IFormatterElement f : formatters) {
  453            f.finish();
  454        }
  455    }
  456
  457    private void createReport(final IReportFormatter formatter)
  458            throws IOException {
  459        final CoverageNodeImpl node = createNode(structure);
  460        final IReportVisitor visitor = formatter.createReportVisitor(node,
  461                sessionInfoStore.getInfos(), executionDataStore.getContents());
  462        final SourceFileCollection sourceFileLocator = new SourceFileCollection(
  463                structure.sourcefiles);
  464        if (node instanceof BundleCoverage) {
  465            visitBundle(visitor, (BundleCoverage) node, sourceFileLocator);
  466        } else {
  467            for (final GroupElement g : structure.children) {
  468                createReport(g, visitor, node);
  469            }
  470        }
  471        visitor.visitEnd(sourceFileLocator);
  472    }
  473
  474    private void createReport(final GroupElement group,
  475            final IReportVisitor parentVisitor,
  476            final CoverageNodeImpl parentNode) throws IOException {
  477        final CoverageNodeImpl node = createNode(group);
  478        final IReportVisitor visitor = parentVisitor.visitChild(node);
  479        final SourceFileCollection sourceFileLocator = new SourceFileCollection(
  480                group.sourcefiles);
  481        if (node instanceof BundleCoverage) {
  482            visitBundle(visitor, (BundleCoverage) node, sourceFileLocator);
  483        } else {
  484            for (final GroupElement g : group.children) {
  485                createReport(g, visitor, node);
  486            }
  487        }
  488        parentNode.increment(node);
  489        visitor.visitEnd(sourceFileLocator);
  490    }
  491
  492    private CoverageNodeImpl createNode(final GroupElement group)
  493            throws IOException {
  494        if (group.name == null) {
  495            throw new BuildException("Group name must be supplied");
  496        }
  497        if (group.children.size() > 0) {
  498            return new CoverageNodeImpl(ElementType.GROUP, group.name, false);
  499        } else {
  500            final CoverageBuilder builder = new CoverageBuilder(
  501                    executionDataStore);
  502            final Analyzer analyzer = new Analyzer(builder);
  503            for (final Iterator<?> i = group.classfiles.iterator(); i.hasNext();) {
  504                final Resource resource = (Resource) i.next();
  505                if (resource.isDirectory() && resource instanceof FileResource) {
  506                    analyzer.analyzeAll(((FileResource) resource).getFile());
  507                } else {
  508                    final InputStream in = resource.getInputStream();
  509                    analyzer.analyzeAll(in);
  510                    in.close();
  511                }
  512            }
  513            return builder.getBundle(group.name);
  514        }
  515    }
  516
  517    private static class SourceFileCollection implements ISourceFileLocator {
  518
  519        private final String encoding;
  520
  521        private final Map<String, Resource> resources = new HashMap<String, Resource>();
  522
  523        SourceFileCollection(final SourceFilesElement sourceFiles) {
  524            encoding = sourceFiles.encoding;
  525            for (final Iterator<?> i = sourceFiles.iterator(); i.hasNext();) {
  526                final Resource r = (Resource) i.next();
  527                resources.put(r.getName().replace(File.separatorChar, '/'), r);
  528            }
  529        }
  530
  531        public Reader getSourceFile(final String packageName,
  532                final String fileName) throws IOException {
  533            final Resource r = resources.get(packageName + '/' + fileName);
  534            if (r == null) {
  535                return null;
  536            }
  537            if (encoding == null) {
  538                return new InputStreamReader(r.getInputStream());
  539            } else {
  540                return new InputStreamReader(r.getInputStream(), encoding);
  541            }
  542        }
  543
  544        public boolean isEmpty() {
  545            return resources.isEmpty();
  546        }
  547    }
  548
  549    private void visitBundle(final IReportVisitor visitor,
  550            final BundleCoverage bundledata,
  551            final SourceFileCollection sourceFileLocator) throws IOException {
  552        if (!sourceFileLocator.isEmpty()) {
  553            checkForMissingDebugInformation(bundledata);
  554        }
  555        for (final PackageCoverage p : bundledata.getPackages()) {
  556            visitPackage(visitor.visitChild(p), p, sourceFileLocator);
  557        }
  558    }
  559
  560    private void checkForMissingDebugInformation(final ICoverageNode node) {
  561        if (node.getClassCounter().getTotalCount() > 0
  562                && node.getLineCounter().getTotalCount() == 0) {
  563            log(format(
  564                    "To enable source code annotation class files for bundle '%s' have to be compiled with debug information.",
  565                    node.getName()), Project.MSG_WARN);
  566        }
  567    }
  568
  569    private static void visitPackage(final IReportVisitor visitor,
  570            final PackageCoverage packagedata,
  571            final ISourceFileLocator sourceFileLocator) throws IOException {
  572        visitLeafs(visitor, packagedata.getSourceFiles(), sourceFileLocator);
  573        for (final ClassCoverage c : packagedata.getClasses()) {
  574            visitClass(visitor.visitChild(c), c, sourceFileLocator);
  575        }
  576        visitor.visitEnd(sourceFileLocator);
  577    }
  578
  579    private static void visitClass(final IReportVisitor visitor,
  580            final ClassCoverage classdata,
  581            final ISourceFileLocator sourceFileLocator) throws IOException {
  582        visitLeafs(visitor, classdata.getMethods(), sourceFileLocator);
  583        visitor.visitEnd(sourceFileLocator);
  584    }
  585
  586    private static void visitLeafs(final IReportVisitor visitor,
  587            final Collection<? extends ICoverageNode> leafs,
  588            final ISourceFileLocator sourceFileLocator) throws IOException {
  589        for (final ICoverageNode l : leafs) {
  590            final IReportVisitor child = visitor.visitChild(l);
  591            child.visitEnd(sourceFileLocator);
  592        }
  593    }
  594
  595}