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.
+