From 536663200dc497eb27df655d205bfa32f057340b Mon Sep 17 00:00:00 2001 From: Tim Baverstock Date: Tue, 17 Nov 2009 18:41:06 +0000 Subject: Add basic LCOV format support to EMMA's report generation. In K&R Java(!) And add a simple regression test. --- .../com/vladium/emma/report/IReportEnums.java | 3 +- ant/ant14/com/vladium/emma/report/reportTask.java | 4 +- .../emma/report/AbstractReportGenerator.java | 4 +- .../vladium/emma/report/lcov/ReportGenerator.java | 413 +++++++++++++++++++++ core/res/com/vladium/emma/report/report_usage.res | 2 +- core/res/com/vladium/emma/run_usage.res | 2 +- test.sh | 136 +++++++ 7 files changed, 558 insertions(+), 6 deletions(-) create mode 100644 core/java12/com/vladium/emma/report/lcov/ReportGenerator.java create mode 100755 test.sh 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 @@ interface IReportEnums { "txt", "html", + "lcov", "xml", }; @@ -93,4 +94,4 @@ interface IReportEnums } // 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 @@ final class reportTask extends FileTask implements IReportProperties, IReportEnu 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 , , nested elements", location).fillInStackTrace (); + + ": no report types specified: provide at least one of , , , nested elements", location).fillInStackTrace (); String [] files = getDataPath (true); if ((files == null) || (files.length == 0)) @@ -176,4 +176,4 @@ final class reportTask extends FileTask implements IReportProperties, IReportEnu 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 @@ abstract class AbstractReportGenerator extends AbstractItemVisitor 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 @@ abstract class AbstractReportGenerator extends AbstractItemVisitor 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, - '', + '', "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, - '', + '', "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 <$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 <$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 <$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. + -- cgit v1.2.3