Add basic LCOV format support to EMMA's report generation. In K&R Java(!)
And add a simple regression test.
diff --git a/ant/ant14/com/vladium/emma/report/IReportEnums.java b/ant/ant14/com/vladium/emma/report/IReportEnums.java
index 7b81457..e1d2305 100644
--- a/ant/ant14/com/vladium/emma/report/IReportEnums.java
+++ b/ant/ant14/com/vladium/emma/report/IReportEnums.java
@@ -33,6 +33,7 @@
{
"txt",
"html",
+ "lcov",
"xml",
};
@@ -93,4 +94,4 @@
} // end of nested class
} // end of interface
-// ----------------------------------------------------------------------------
\ No newline at end of file
+// ----------------------------------------------------------------------------
diff --git a/ant/ant14/com/vladium/emma/report/reportTask.java b/ant/ant14/com/vladium/emma/report/reportTask.java
index b726753..931fad4 100644
--- a/ant/ant14/com/vladium/emma/report/reportTask.java
+++ b/ant/ant14/com/vladium/emma/report/reportTask.java
@@ -47,7 +47,7 @@
if ((reportTypes == null) || (reportTypes.length == 0)) // no "txt" default for report processor
throw (BuildException) newBuildException (getTaskName ()
- + ": no report types specified: provide at least one of <txt>, <html>, <xml> nested elements", location).fillInStackTrace ();
+ + ": no report types specified: provide at least one of <txt>, <html>, <lcov>, <xml> nested elements", location).fillInStackTrace ();
String [] files = getDataPath (true);
if ((files == null) || (files.length == 0))
@@ -176,4 +176,4 @@
private ReportCfg m_reportCfg;
} // end of class
-// ----------------------------------------------------------------------------
\ No newline at end of file
+// ----------------------------------------------------------------------------
diff --git a/core/java12/com/vladium/emma/report/AbstractReportGenerator.java b/core/java12/com/vladium/emma/report/AbstractReportGenerator.java
index 6e37179..b0330ee 100644
--- a/core/java12/com/vladium/emma/report/AbstractReportGenerator.java
+++ b/core/java12/com/vladium/emma/report/AbstractReportGenerator.java
@@ -43,6 +43,8 @@
if ("html".equals (type))
return new com.vladium.emma.report.html.ReportGenerator ();
+ if ("lcov".equals (type))
+ return new com.vladium.emma.report.lcov.ReportGenerator ();
else if ("txt".equals (type))
return new com.vladium.emma.report.txt.ReportGenerator ();
else if ("xml".equals (type))
@@ -255,4 +257,4 @@
private static final int MAX_DEBUG_INFO_WARNING_COUNT = 3; // per package
} // end of class
-// ----------------------------------------------------------------------------
\ No newline at end of file
+// ----------------------------------------------------------------------------
diff --git a/core/java12/com/vladium/emma/report/lcov/ReportGenerator.java b/core/java12/com/vladium/emma/report/lcov/ReportGenerator.java
new file mode 100644
index 0000000..08a6310
--- /dev/null
+++ b/core/java12/com/vladium/emma/report/lcov/ReportGenerator.java
@@ -0,0 +1,413 @@
+/* Copyright 2009 Google Inc. All Rights Reserved.
+ * Derived from code Copyright (C) 2003 Vladimir Roubtsov.
+ *
+ * This program and the accompanying materials are made available under
+ * the terms of the Common Public License v1.0 which accompanies this
+ * distribution, and is available at http://www.eclipse.org/legal/cpl-v10.html
+ *
+ * $Id$
+ */
+
+package com.vladium.emma.report.lcov;
+
+import com.vladium.emma.EMMARuntimeException;
+import com.vladium.emma.IAppErrorCodes;
+import com.vladium.emma.data.ClassDescriptor;
+import com.vladium.emma.data.ICoverageData;
+import com.vladium.emma.data.IMetaData;
+import com.vladium.emma.report.AbstractReportGenerator;
+import com.vladium.emma.report.AllItem;
+import com.vladium.emma.report.ClassItem;
+import com.vladium.emma.report.IItem;
+import com.vladium.emma.report.ItemComparator;
+import com.vladium.emma.report.MethodItem;
+import com.vladium.emma.report.PackageItem;
+import com.vladium.emma.report.SourcePathCache;
+import com.vladium.emma.report.SrcFileItem;
+import com.vladium.util.Descriptors;
+import com.vladium.util.Files;
+import com.vladium.util.IProperties;
+import com.vladium.util.IntObjectMap;
+import com.vladium.util.asserts.$assert;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+import java.util.Iterator;
+import java.util.LinkedList;
+
+/**
+ * @author Vlad Roubtsov, (C) 2003
+ * @author Tim Baverstock, (C) 2009
+ *
+ * Generates LCOV format files:
+ * http://manpages.ubuntu.com/manpages/karmic/man1/geninfo.1.html
+ */
+public final class ReportGenerator extends AbstractReportGenerator
+ implements IAppErrorCodes
+{
+ public String getType()
+ {
+ return TYPE;
+ }
+
+ /**
+ * Queue-based visitor, starts with the root, node visits enqueue child
+ * nodes.
+ */
+ public void process(final IMetaData mdata,
+ final ICoverageData cdata,
+ final SourcePathCache cache,
+ final IProperties properties)
+ throws EMMARuntimeException
+ {
+ initialize(mdata, cdata, cache, properties);
+
+ long start = 0;
+ long end;
+ final boolean trace1 = m_log.atTRACE1();
+
+ if (trace1)
+ {
+ start = System.currentTimeMillis();
+ }
+
+ m_queue = new LinkedList();
+ for (m_queue.add(m_view.getRoot()); !m_queue.isEmpty(); )
+ {
+ final IItem head = (IItem) m_queue.removeFirst();
+ head.accept(this, null);
+ }
+ close();
+
+ if (trace1)
+ {
+ end = System.currentTimeMillis();
+ m_log.trace1("process", "[" + getType() + "] report generated in "
+ + (end - start) + " ms");
+ }
+ }
+
+ public void cleanup()
+ {
+ m_queue = null;
+ close();
+ super.cleanup();
+ }
+
+
+ /**
+ * Visitor for top-level node; opens output file, enqueues packages.
+ */
+ public Object visit(final AllItem item, final Object ctx)
+ {
+ File outFile = m_settings.getOutFile();
+ if (outFile == null)
+ {
+ outFile = new File("coverage.lcov");
+ m_settings.setOutFile(outFile);
+ }
+
+ final File fullOutFile = Files.newFile(m_settings.getOutDir(), outFile);
+
+ m_log.info("writing [" + getType() + "] report to ["
+ + fullOutFile.getAbsolutePath() + "] ...");
+
+ openOutFile(fullOutFile, m_settings.getOutEncoding(), true);
+
+ // Enqueue packages
+ final ItemComparator order =
+ m_typeSortComparators[PackageItem.getTypeMetadata().getTypeID()];
+ for (Iterator packages = item.getChildren(order); packages.hasNext(); )
+ {
+ final IItem pkg = (IItem) packages.next();
+ m_queue.addLast(pkg);
+ }
+
+ return ctx;
+ }
+
+ /**
+ * Visitor for packages; enqueues source files contained by the package.
+ */
+ public Object visit(final PackageItem item, final Object ctx)
+ {
+ if (m_verbose)
+ {
+ m_log.verbose(" report: processing package [" + item.getName() + "] ...");
+ }
+
+ // Enqueue source files
+ int id = m_srcView
+ ? SrcFileItem.getTypeMetadata().getTypeID()
+ : ClassItem.getTypeMetadata().getTypeID();
+ final ItemComparator order = m_typeSortComparators[id];
+ for (Iterator srcORclsFiles = item.getChildren(order);
+ srcORclsFiles.hasNext();
+ )
+ {
+ final IItem srcORcls = (IItem) srcORclsFiles.next();
+ m_queue.addLast(srcORcls);
+ }
+
+ return ctx;
+ }
+
+ /**
+ * Visitor for source files: doesn't use the enqueue mechanism to examine
+ * deeper nodes because it writes the 'end_of_record' decoration here.
+ */
+ public Object visit (final SrcFileItem item, final Object ctx)
+ {
+ row("SF:".concat(item.getFullVMName()));
+
+ // TODO: Enqueue ClassItems, then an 'end_of_record' object
+
+ emitFileCoverage(item);
+
+ row("end_of_record");
+ return ctx;
+ }
+
+ /** Issue a coverage report for all lines in the file, and for each
+ * function in the file.
+ */
+ private void emitFileCoverage(final SrcFileItem item)
+ {
+ if ($assert.ENABLED)
+ {
+ $assert.ASSERT(item != null, "null input: item");
+ }
+
+ final String fileName = item.getFullVMName();
+
+ final String packageVMName = ((PackageItem) item.getParent()).getVMName();
+
+ if (!m_hasLineNumberInfo)
+ {
+ m_log.info("source file '"
+ + Descriptors.combineVMName(packageVMName, fileName)
+ + "' has no line number information");
+ }
+ boolean success = false;
+
+ try
+ {
+ // For each class in the file, for each method in the class,
+ // examine the execution blocks in the method until one with
+ // coverage is found. Report coverage or non-coverage on the
+ // strength of that one block (much as for now, a line is 'covered'
+ // if it's partially covered).
+
+ // TODO: Intertwingle method records and line records
+
+ {
+ final ItemComparator order = m_typeSortComparators[
+ ClassItem.getTypeMetadata().getTypeID()];
+ int clsIndex = 0;
+ for (Iterator classes = item.getChildren(order);
+ classes.hasNext();
+ ++clsIndex)
+ {
+ final ClassItem cls = (ClassItem) classes.next();
+
+ final String className = cls.getName();
+
+ ClassDescriptor cdesc = cls.getClassDescriptor();
+
+ // [methodid][blocksinmethod]
+ boolean[][] ccoverage = cls.getCoverage();
+
+ final ItemComparator order2 = m_typeSortComparators[
+ MethodItem.getTypeMetadata().getTypeID()];
+ for (Iterator methods = cls.getChildren(order2); methods.hasNext(); )
+ {
+ final MethodItem method = (MethodItem) methods.next();
+ String mname = method.getName();
+ final int methodID = method.getID();
+
+ boolean covered = false;
+ if (ccoverage != null)
+ {
+ if ($assert.ENABLED)
+ {
+ $assert.ASSERT(ccoverage.length > methodID, "index bounds");
+ $assert.ASSERT(ccoverage[methodID] != null, "null: coverage");
+ $assert.ASSERT(ccoverage[methodID].length > 0, "empty array");
+ }
+ covered = ccoverage[methodID][0];
+ }
+
+ row("FN:" + method.getFirstLine() + "," + className + "::" + mname);
+ row("FNDA:" + (covered ? 1 : 0) + "," + className + "::" + mname);
+ }
+ }
+ }
+
+ // For each line in the file, emit a DA.
+
+ {
+ final int unitsType = m_settings.getUnitsType();
+ // line num:int -> SrcFileItem.LineCoverageData
+ IntObjectMap lineCoverageMap = null;
+ int[] lineCoverageKeys = null;
+
+ lineCoverageMap = item.getLineCoverage();
+ $assert.ASSERT(lineCoverageMap != null, "null: lineCoverageMap");
+ lineCoverageKeys = lineCoverageMap.keys();
+ java.util.Arrays.sort(lineCoverageKeys);
+
+ for (int i = 0; i < lineCoverageKeys.length; ++i)
+ {
+ int l = lineCoverageKeys[i];
+ final SrcFileItem.LineCoverageData lCoverageData =
+ (SrcFileItem.LineCoverageData) lineCoverageMap.get(l);
+
+ if ($assert.ENABLED)
+ {
+ $assert.ASSERT(lCoverageData != null, "lCoverage is null");
+ }
+ switch (lCoverageData.m_coverageStatus)
+ {
+ case SrcFileItem.LineCoverageData.LINE_COVERAGE_ZERO:
+ row("DA:" + l + ",0");
+ break;
+
+ case SrcFileItem.LineCoverageData.LINE_COVERAGE_PARTIAL:
+ // TODO: Add partial coverage support to LCOV
+ row("DA:" + l + ",1");
+ break;
+
+ case SrcFileItem.LineCoverageData.LINE_COVERAGE_COMPLETE:
+ row("DA:" + l + ",1");
+ break;
+
+ default:
+ $assert.ASSERT(false, "invalid line coverage status: "
+ + lCoverageData.m_coverageStatus);
+
+ } // end of switch
+ }
+ }
+
+ success = true;
+ }
+ catch (Throwable t)
+ {
+ t.printStackTrace(System.out);
+ success = false;
+ }
+
+ if (!success)
+ {
+ m_log.info("[source file '"
+ + Descriptors.combineVMName(packageVMName, fileName)
+ + "' not found in sourcepath]");
+ }
+ }
+
+ public Object visit (final ClassItem item, final Object ctx)
+ {
+ return ctx;
+ }
+
+ private void row(final StringBuffer str)
+ {
+ if ($assert.ENABLED)
+ {
+ $assert.ASSERT(str != null, "str = null");
+ }
+
+ try
+ {
+ m_out.write(str.toString());
+ m_out.newLine();
+ }
+ catch (IOException ioe)
+ {
+ throw new EMMARuntimeException(IAppErrorCodes.REPORT_IO_FAILURE, ioe);
+ }
+ }
+
+ private void row(final String str)
+ {
+ if ($assert.ENABLED)
+ {
+ $assert.ASSERT(str != null, "str = null");
+ }
+
+ try
+ {
+ m_out.write(str);
+ m_out.newLine();
+ }
+ catch (IOException ioe)
+ {
+ throw new EMMARuntimeException(IAppErrorCodes.REPORT_IO_FAILURE, ioe);
+ }
+ }
+
+ private void close()
+ {
+ if (m_out != null)
+ {
+ try
+ {
+ m_out.flush();
+ m_out.close();
+ }
+ catch (IOException ioe)
+ {
+ throw new EMMARuntimeException(IAppErrorCodes.REPORT_IO_FAILURE, ioe);
+ }
+ finally
+ {
+ m_out = null;
+ }
+ }
+ }
+
+ private void openOutFile(final File file, final String encoding, final boolean mkdirs)
+ {
+ try
+ {
+ if (mkdirs)
+ {
+ final File parent = file.getParentFile();
+ if (parent != null)
+ {
+ parent.mkdirs();
+ }
+ }
+ file.delete();
+ if (file.exists())
+ {
+ throw new EMMARuntimeException("Failed to delete " + file);
+ }
+ m_out = new BufferedWriter(
+ new OutputStreamWriter(new FileOutputStream(file), encoding),
+ IO_BUF_SIZE);
+ }
+ catch (UnsupportedEncodingException uee)
+ {
+ throw new EMMARuntimeException(uee);
+ }
+ catch (IOException fnfe) // FileNotFoundException
+ {
+ // note: in J2SDK 1.3 FileOutputStream constructor's throws clause
+ // was narrowed to FileNotFoundException:
+ throw new EMMARuntimeException(fnfe);
+ }
+ }
+
+ private LinkedList /* IITem */ m_queue;
+ private BufferedWriter m_out;
+
+ private static final String TYPE = "lcov";
+
+ private static final int IO_BUF_SIZE = 32 * 1024;
+}
+
diff --git a/core/res/com/vladium/emma/report/report_usage.res b/core/res/com/vladium/emma/report/report_usage.res
index f91c241..efb56e1 100644
--- a/core/res/com/vladium/emma/report/report_usage.res
+++ b/core/res/com/vladium/emma/report/report_usage.res
@@ -6,7 +6,7 @@
'r', 'report':
required, mergeable, values: 1,
- '<list of {txt|html|xml}>',
+ '<list of {txt|html|lcov|xml}>',
"coverage report type list";
'sp', 'sourcepath':
diff --git a/core/res/com/vladium/emma/run_usage.res b/core/res/com/vladium/emma/run_usage.res
index 42d6f7c..c04c430 100644
--- a/core/res/com/vladium/emma/run_usage.res
+++ b/core/res/com/vladium/emma/run_usage.res
@@ -21,7 +21,7 @@
'r', 'report':
optional, mergeable, values: 1,
- '<list of {txt|html|xml}>',
+ '<list of {txt|html|lcov|xml}>',
"coverage report type list";
'sp', 'sourcepath':
diff --git a/test.sh b/test.sh
new file mode 100755
index 0000000..73b91cd
--- /dev/null
+++ b/test.sh
@@ -0,0 +1,136 @@
+#!/bin/bash
+#
+# Copyright 2009 Google Inc. All Rights Reserved.
+# Author: weasel@google.com (Tim Baverstock)
+#
+# This program and the accompanying materials are made available under
+# the terms of the Common Public License v1.0 which accompanies this
+# distribution, and is available at http://www.eclipse.org/legal/cpl-v10.html
+#
+# This script tests the emma jar from the sources in this directory.
+# This script has to be run from its current directory ONLY.
+# Sample usages:
+# To just test emma.jar:
+# ./test.sh
+
+TESTDIR=/tmp/test-emma/$$
+JAVADIR=$TESTDIR/android3/java
+SOURCEDIR=$JAVADIR/com/android/bunnies
+mkdir -p $SOURCEDIR
+
+cat <<END >$SOURCEDIR/Bunny.java
+package com.android.bunnies;
+
+import java.util.Random;
+
+public class Bunny {
+ int randomNumber1 = (new Random()).nextInt();
+
+ int randomNumber2;
+
+ {
+ Random r = new Random();
+ randomNumber2 = r.nextInt();
+ }
+
+ int addOne(int a) {
+ int b = a + 1;
+ return identity(a + 1)
+ ? 1
+ : 0;
+ }
+
+ int dontAddOne(int a) {
+ return a;
+ }
+
+ boolean identity(int a) {
+ return a != a;
+ }
+
+ public static void main(String[] args) {
+ Bunny thisThing = new Bunny();
+ SubBunny thatThing = new SubBunny();
+ System.out.println(thisThing.addOne(2));
+ System.out.println(thatThing.addOne(2));
+ }
+}
+END
+cat <<END >$SOURCEDIR/SubBunny.java
+package com.android.bunnies;
+import com.android.bunnies.Bunny;
+class SubBunny extends Bunny {
+ int addOne(int a) {
+ int b = a + 2;
+ return identity(a) && identity(b) || identity(b)
+ ? 1
+ : 0;
+ }
+
+ boolean identity(int a) {
+ return a == a;
+ }
+}
+END
+
+GOLDEN=$TESTDIR/golden.lcov
+cat <<END >$GOLDEN
+SF:com/android/bunnies/SubBunny.java
+FN:5,SubBunny::addOne (int): int
+FNDA:1,SubBunny::addOne (int): int
+FN:12,SubBunny::identity (int): boolean
+FNDA:1,SubBunny::identity (int): boolean
+FN:3,SubBunny::SubBunny (): void
+FNDA:1,SubBunny::SubBunny (): void
+DA:3,1
+DA:5,1
+DA:6,1
+DA:12,1
+end_of_record
+SF:com/android/bunnies/Bunny.java
+FN:23,Bunny::dontAddOne (int): int
+FNDA:0,Bunny::dontAddOne (int): int
+FN:27,Bunny::identity (int): boolean
+FNDA:1,Bunny::identity (int): boolean
+FN:16,Bunny::addOne (int): int
+FNDA:1,Bunny::addOne (int): int
+FN:5,Bunny::Bunny (): void
+FNDA:1,Bunny::Bunny (): void
+FN:31,Bunny::main (String []): void
+FNDA:1,Bunny::main (String []): void
+DA:5,1
+DA:6,1
+DA:11,1
+DA:12,1
+DA:13,1
+DA:16,1
+DA:17,1
+DA:23,0
+DA:27,1
+DA:31,1
+DA:32,1
+DA:33,1
+DA:34,1
+DA:35,1
+end_of_record
+END
+
+javac -g $(find $SOURCEDIR -name \*.java)
+
+COVERAGE=$TESTDIR/coverage.dat
+java -cp dist/emma.jar emmarun -r lcov -cp $JAVADIR \
+ -sp $JAVADIR -Dreport.lcov.out.file=$COVERAGE com.android.bunnies.Bunny
+
+# Don't really need to test these separately, but it's useful to me for now.
+
+if ! diff <(sort $GOLDEN) <(sort $COVERAGE) >$TESTDIR/diff-sorted; then
+ echo Tests failed: Additional or missing lines: See $TESTDIR/diff-sorted
+ exit
+fi
+if ! diff $GOLDEN $COVERAGE >$TESTDIR/diff-ordered; then
+ echo Tests failed: same lines, different order: See $TESTDIR/diff-ordered
+ exit
+fi
+rm -rf $TESTDIR
+echo Tests passed.
+