1/* Copyright 2009 Google Inc. All Rights Reserved.
2 * Derived from code Copyright (C) 2003 Vladimir Roubtsov.
3 *
4 * This program and the accompanying materials are made available under
5 * the terms of the Common Public License v1.0 which accompanies this
6 * distribution, and is available at http://www.eclipse.org/legal/cpl-v10.html
7 *
8 * $Id$
9 */
10
11package com.vladium.emma.report.lcov;
12
13import com.vladium.emma.EMMARuntimeException;
14import com.vladium.emma.IAppErrorCodes;
15import com.vladium.emma.data.ClassDescriptor;
16import com.vladium.emma.data.ICoverageData;
17import com.vladium.emma.data.IMetaData;
18import com.vladium.emma.report.AbstractReportGenerator;
19import com.vladium.emma.report.AllItem;
20import com.vladium.emma.report.ClassItem;
21import com.vladium.emma.report.IItem;
22import com.vladium.emma.report.ItemComparator;
23import com.vladium.emma.report.MethodItem;
24import com.vladium.emma.report.PackageItem;
25import com.vladium.emma.report.SourcePathCache;
26import com.vladium.emma.report.SrcFileItem;
27import com.vladium.util.Descriptors;
28import com.vladium.util.Files;
29import com.vladium.util.IProperties;
30import com.vladium.util.IntObjectMap;
31import com.vladium.util.asserts.$assert;
32
33import java.io.BufferedWriter;
34import java.io.File;
35import java.io.FileOutputStream;
36import java.io.IOException;
37import java.io.OutputStreamWriter;
38import java.io.UnsupportedEncodingException;
39import java.util.Iterator;
40import java.util.LinkedList;
41
42/**
43 * @author Vlad Roubtsov, (C) 2003
44 * @author Tim Baverstock, (C) 2009
45 *
46 * Generates LCOV format files:
47 *    http://manpages.ubuntu.com/manpages/karmic/man1/geninfo.1.html
48 */
49public final class ReportGenerator extends AbstractReportGenerator
50                                   implements IAppErrorCodes
51{
52    public String getType()
53    {
54        return TYPE;
55    }
56
57    /**
58     * Queue-based visitor, starts with the root, node visits enqueue child
59     * nodes.
60     */
61    public void process(final IMetaData mdata,
62                        final ICoverageData cdata,
63                        final SourcePathCache cache,
64                        final IProperties properties)
65        throws EMMARuntimeException
66    {
67        initialize(mdata, cdata, cache, properties);
68
69        long start = 0;
70        long end;
71        final boolean trace1 = m_log.atTRACE1();
72
73        if (trace1)
74        {
75            start = System.currentTimeMillis();
76        }
77
78        m_queue = new LinkedList();
79        for (m_queue.add(m_view.getRoot()); !m_queue.isEmpty(); )
80        {
81            final IItem head = (IItem) m_queue.removeFirst();
82            head.accept(this, null);
83        }
84        close();
85
86        if (trace1)
87        {
88            end = System.currentTimeMillis();
89            m_log.trace1("process", "[" + getType() + "] report generated in "
90                         + (end - start) + " ms");
91        }
92    }
93
94    public void cleanup()
95    {
96        m_queue = null;
97        close();
98        super.cleanup();
99    }
100
101
102    /**
103    * Visitor for top-level node; opens output file, enqueues packages.
104    */
105    public Object visit(final AllItem item, final Object ctx)
106    {
107        File outFile = m_settings.getOutFile();
108        if (outFile == null)
109        {
110            outFile = new File("coverage.lcov");
111            m_settings.setOutFile(outFile);
112        }
113
114        final File fullOutFile = Files.newFile(m_settings.getOutDir(), outFile);
115
116        m_log.info("writing [" + getType() + "] report to ["
117                   + fullOutFile.getAbsolutePath() + "] ...");
118
119        openOutFile(fullOutFile, m_settings.getOutEncoding(), true);
120
121        // Enqueue packages
122        final ItemComparator order =
123                m_typeSortComparators[PackageItem.getTypeMetadata().getTypeID()];
124        for (Iterator packages = item.getChildren(order); packages.hasNext(); )
125        {
126            final IItem pkg = (IItem) packages.next();
127            m_queue.addLast(pkg);
128        }
129
130        return ctx;
131    }
132
133    /**
134     * Visitor for packages; enqueues source files contained by the package.
135     */
136    public Object visit(final PackageItem item, final Object ctx)
137    {
138        if (m_verbose)
139        {
140            m_log.verbose("  report: processing package [" + item.getName() + "] ...");
141        }
142
143        // Enqueue source files
144        int id = m_srcView
145                 ? SrcFileItem.getTypeMetadata().getTypeID()
146                 : ClassItem.getTypeMetadata().getTypeID();
147        final ItemComparator order = m_typeSortComparators[id];
148        for (Iterator srcORclsFiles = item.getChildren(order);
149             srcORclsFiles.hasNext();
150            )
151        {
152            final IItem srcORcls = (IItem) srcORclsFiles.next();
153            m_queue.addLast(srcORcls);
154        }
155
156        return ctx;
157    }
158
159    /**
160     * Visitor for source files: doesn't use the enqueue mechanism to examine
161     * deeper nodes because it writes the 'end_of_record' decoration here.
162     */
163    public Object visit (final SrcFileItem item, final Object ctx)
164    {
165        row("SF:".concat(item.getFullVMName()));
166
167        // TODO: Enqueue ClassItems, then an 'end_of_record' object
168
169        emitFileCoverage(item);
170
171        row("end_of_record");
172        return ctx;
173    }
174
175    /** Issue a coverage report for all lines in the file, and for each
176     * function in the file.
177     */
178    private void emitFileCoverage(final SrcFileItem item)
179    {
180        if ($assert.ENABLED)
181        {
182            $assert.ASSERT(item != null, "null input: item");
183        }
184
185        final String fileName = item.getFullVMName();
186
187        final String packageVMName = ((PackageItem) item.getParent()).getVMName();
188
189        if (!m_hasLineNumberInfo)
190        {
191            m_log.info("source file '"
192                       + Descriptors.combineVMName(packageVMName, fileName)
193                       + "' has no line number information");
194        }
195        boolean success = false;
196
197        try
198        {
199            // For each class in the file, for each method in the class,
200            // examine the execution blocks in the method until one with
201            // coverage is found. Report coverage or non-coverage on the
202            // strength of that one block (much as for now, a line is 'covered'
203            // if it's partially covered).
204
205            // TODO: Intertwingle method records and line records
206
207            {
208                final ItemComparator order = m_typeSortComparators[
209                        ClassItem.getTypeMetadata().getTypeID()];
210                int clsIndex = 0;
211                for (Iterator classes = item.getChildren(order);
212                     classes.hasNext();
213                     ++clsIndex)
214                {
215                    final ClassItem cls = (ClassItem) classes.next();
216
217                    final String className = cls.getName();
218
219                    ClassDescriptor cdesc = cls.getClassDescriptor();
220
221                    // [methodid][blocksinmethod]
222                    boolean[][] ccoverage = cls.getCoverage();
223
224                    final ItemComparator order2 = m_typeSortComparators[
225                            MethodItem.getTypeMetadata().getTypeID()];
226                    for (Iterator methods = cls.getChildren(order2); methods.hasNext(); )
227                    {
228                        final MethodItem method = (MethodItem) methods.next();
229                        String mname = method.getName();
230                        final int methodID = method.getID();
231
232                        boolean covered = false;
233                        if (ccoverage != null)
234                        {
235                            if ($assert.ENABLED)
236                            {
237                                $assert.ASSERT(ccoverage.length > methodID, "index bounds");
238                                $assert.ASSERT(ccoverage[methodID] != null, "null: coverage");
239                                $assert.ASSERT(ccoverage[methodID].length > 0, "empty array");
240                            }
241                            covered = ccoverage[methodID][0];
242                        }
243
244                        row("FN:" + method.getFirstLine() + "," + className + "::" + mname);
245                        row("FNDA:" + (covered ? 1 : 0) + "," + className + "::" + mname);
246                    }
247                }
248            }
249
250            // For each line in the file, emit a DA.
251
252            {
253                final int unitsType = m_settings.getUnitsType();
254                // line num:int -> SrcFileItem.LineCoverageData
255                IntObjectMap lineCoverageMap = null;
256                int[] lineCoverageKeys = null;
257
258                lineCoverageMap = item.getLineCoverage();
259                $assert.ASSERT(lineCoverageMap != null, "null: lineCoverageMap");
260                lineCoverageKeys = lineCoverageMap.keys();
261                java.util.Arrays.sort(lineCoverageKeys);
262
263                for (int i = 0; i < lineCoverageKeys.length; ++i)
264                {
265                    int l = lineCoverageKeys[i];
266                    final SrcFileItem.LineCoverageData lCoverageData =
267                            (SrcFileItem.LineCoverageData) lineCoverageMap.get(l);
268
269                    if ($assert.ENABLED)
270                    {
271                        $assert.ASSERT(lCoverageData != null, "lCoverage is null");
272                    }
273                    switch (lCoverageData.m_coverageStatus)
274                    {
275                        case SrcFileItem.LineCoverageData.LINE_COVERAGE_ZERO:
276                            row("DA:" + l + ",0");
277                            break;
278
279                        case SrcFileItem.LineCoverageData.LINE_COVERAGE_PARTIAL:
280                            // TODO: Add partial coverage support to LCOV
281                            row("DA:" + l + ",1");
282                            break;
283
284                        case SrcFileItem.LineCoverageData.LINE_COVERAGE_COMPLETE:
285                            row("DA:" + l + ",1");
286                            break;
287
288                        default:
289                            $assert.ASSERT(false, "invalid line coverage status: "
290                                           + lCoverageData.m_coverageStatus);
291
292                    } // end of switch
293                }
294            }
295
296            success = true;
297        }
298        catch (Throwable t)
299        {
300            t.printStackTrace(System.out);
301            success = false;
302        }
303
304        if (!success)
305        {
306            m_log.info("[source file '"
307                       + Descriptors.combineVMName(packageVMName, fileName)
308                       + "' not found in sourcepath]");
309        }
310    }
311
312    public Object visit (final ClassItem item, final Object ctx)
313    {
314        return ctx;
315    }
316
317    private void row(final StringBuffer str)
318    {
319        if ($assert.ENABLED)
320        {
321            $assert.ASSERT(str != null, "str = null");
322        }
323
324        try
325        {
326            m_out.write(str.toString());
327            m_out.newLine();
328        }
329        catch (IOException ioe)
330        {
331            throw new EMMARuntimeException(IAppErrorCodes.REPORT_IO_FAILURE, ioe);
332        }
333    }
334
335    private void row(final String str)
336    {
337        if ($assert.ENABLED)
338        {
339            $assert.ASSERT(str != null, "str = null");
340        }
341
342        try
343        {
344            m_out.write(str);
345            m_out.newLine();
346        }
347        catch (IOException ioe)
348        {
349            throw new EMMARuntimeException(IAppErrorCodes.REPORT_IO_FAILURE, ioe);
350        }
351    }
352
353    private void close()
354    {
355        if (m_out != null)
356        {
357            try
358            {
359                m_out.flush();
360                m_out.close();
361            }
362            catch (IOException ioe)
363            {
364                throw new EMMARuntimeException(IAppErrorCodes.REPORT_IO_FAILURE, ioe);
365            }
366            finally
367            {
368                m_out = null;
369            }
370        }
371    }
372
373    private void openOutFile(final File file, final String encoding, final boolean mkdirs)
374    {
375        try
376        {
377            if (mkdirs)
378            {
379                final File parent = file.getParentFile();
380                if (parent != null)
381                {
382                    parent.mkdirs();
383                }
384            }
385            file.delete();
386            if (file.exists())
387            {
388                throw new EMMARuntimeException("Failed to delete " + file);
389            }
390            m_out = new BufferedWriter(
391                    new OutputStreamWriter(new FileOutputStream(file), encoding),
392                    IO_BUF_SIZE);
393        }
394        catch (UnsupportedEncodingException uee)
395        {
396            throw new EMMARuntimeException(uee);
397        }
398        catch (IOException fnfe) // FileNotFoundException
399        {
400            // note: in J2SDK 1.3 FileOutputStream constructor's throws clause
401            // was narrowed to FileNotFoundException:
402            throw new EMMARuntimeException(fnfe);
403        }
404    }
405
406    private LinkedList /* IITem */ m_queue;
407    private BufferedWriter m_out;
408
409    private static final String TYPE = "lcov";
410
411    private static final int IO_BUF_SIZE = 32 * 1024;
412}
413
414