/*
* Cobertura - http://cobertura.sourceforge.net/
*
* Copyright (C) 2011 Piotr Tabor
*
* Note: This file is dual licensed under the GPL and the Apache
* Source License (so that it can be used from both the main
* Cobertura classes and the ant tasks).
*
* Cobertura is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published
* by the Free Software Foundation; either version 2 of the License,
* or (at your option) any later version.
*
* Cobertura is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Cobertura; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
* USA
*/
package net.sourceforge.cobertura.instrument;
import net.sourceforge.cobertura.coveragedata.ProjectData;
import net.sourceforge.cobertura.instrument.pass1.DetectDuplicatedCodeClassVisitor;
import net.sourceforge.cobertura.instrument.pass1.DetectIgnoredCodeClassVisitor;
import net.sourceforge.cobertura.instrument.pass2.BuildClassMapClassVisitor;
import net.sourceforge.cobertura.instrument.pass3.InjectCodeClassInstrumenter;
import net.sourceforge.cobertura.util.IOUtil;
import org.apache.log4j.Logger;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.util.CheckClassAdapter;
import java.io.*;
import java.util.*;
import java.util.regex.Pattern;
/**
* Class that is responsible for the whole process of instrumentation of a single class.
* <p/>
* The class is instrumented in tree passes:
* <ol>
* <li>Read only: {@link DetectDuplicatedCodeClassVisitor} - we look for the same ASM code snippets
* rendered in different places of destination code</li>
* <li>Read only: {@link BuildClassMapClassVisitor} - finds all touch-points and other interesting
* information that are in the class and store it in {@link ClassMap}.
* <li>Real instrumentation: {@link InjectCodeClassInstrumenter}. Uses {#link ClassMap} to inject
* code into the class</li>
* </ol>
*
* @author piotr.tabor@gmail.com
*/
public class CoberturaInstrumenter {
private static final Logger logger = Logger
.getLogger(CoberturaInstrumenter.class);
/**
* During the instrumentation process we are feeling {@link ProjectData}, to generate from
* it the *.ser file.
* <p/>
* We now (1.10+) don't need to generate the file (it is not necessery for reporting), but we still
* do it for backward compatibility (for example maven-cobertura-plugin expects it). We should avoid
* this some day.
*/
private ProjectData projectData;
/**
* The root directory for instrumented classes. If it is null, the instrumented classes are overwritten.
*/
private File destinationDirectory;
/**
* List of patterns to know that we don't want trace lines that are calls to some methods
*/
private Collection<Pattern> ignoreRegexes = new Vector<Pattern>();
/**
* Methods annotated by this annotations will be ignored during coverage measurement
*/
private Set<String> ignoreMethodAnnotations = new HashSet<String>();
/**
* If true: Getters, Setters and simple initialization will be ignored by coverage measurement
*/
private boolean ignoreTrivial;
/**
* If true: The process is interrupted when first error occurred.
*/
private boolean failOnError;
/**
* If true: We make sure to keep track of test unit that execute which individual line.
*/
private boolean individualTest;
/**
* Setting to true causes cobertura to use more strict threadsafe model that is significantly
* slower, but guarantees that number of hits counted for each line will be precise in multithread-environment.
* <p/>
* The option does not change measured coverage.
* <p/>
* In implementation it means that AtomicIntegerArray will be used instead of int[].
*/
private boolean threadsafeRigorous;
/**
* Used for storing all the test unit locations.
*/
private List<File> testUnitFilePath;
/**
* Analyzes and instruments class given by path.
* <p/>
* <p>Also the {@link #projectData} structure is filled with information about the found touch-points</p>
*
* @param file - path to class that should be instrumented
*
* @return instrumentation result structure or null in case of problems
*/
public InstrumentationResult instrumentClass(File file) {
InputStream inputStream = null;
try {
logger.debug("Working on file:" + file.getAbsolutePath());
inputStream = new FileInputStream(file);
return instrumentClass(inputStream);
} catch (Throwable t) {
logger.warn("Unable to instrument file " + file.getAbsolutePath(),
t);
if (failOnError) {
throw new RuntimeException(
"Warning detected and failOnError is true", t);
} else {
return null;
}
} finally {
IOUtil.closeInputStream(inputStream);
}
}
/**
* Analyzes and instruments class given by inputStream
* <p/>
* <p>Also the {@link #projectData} structure is filled with information about the found touch-points</p>
*
* @param inputStream - source of class to instrument
*
*
* @return instrumentation result structure or null in case of problems
*/
public InstrumentationResult instrumentClass(InputStream inputStream)
throws IOException {
if (testUnitFilePath != null &&
testUnitFilePath.size() > 0 &&
individualTest) {
new TestUnitInstrumenter(testUnitFilePath);
}
ClassReader cr0 = new ClassReader(inputStream);
ClassWriter cw0 = new ClassWriter(0);
DetectIgnoredCodeClassVisitor detectIgnoredCv = new DetectIgnoredCodeClassVisitor(
cw0, ignoreTrivial, ignoreMethodAnnotations);
DetectDuplicatedCodeClassVisitor cv0 = new DetectDuplicatedCodeClassVisitor(
detectIgnoredCv);
cr0.accept(cv0, 0);
ClassReader cr = new ClassReader(cw0.toByteArray());
ClassWriter cw = new ClassWriter(0);
BuildClassMapClassVisitor cv = new BuildClassMapClassVisitor(cw,
ignoreRegexes, cv0.getDuplicatesLinesCollector(),
detectIgnoredCv.getIgnoredMethodNamesAndSignatures());
cr.accept(cv, ClassReader.EXPAND_FRAMES);
if (logger.isDebugEnabled()) {
logger
.debug("=============== Detected duplicated code =============");
Map<Integer, Map<Integer, Integer>> l = cv0
.getDuplicatesLinesCollector();
for (Map.Entry<Integer, Map<Integer, Integer>> m : l.entrySet()) {
if (m.getValue() != null) {
for (Map.Entry<Integer, Integer> pair : m.getValue()
.entrySet()) {
logger.debug(cv.getClassMap().getClassName() + ":"
+ m.getKey() + " " + pair.getKey() + "->"
+ pair.getValue());
}
}
}
logger
.debug("=============== End of detected duplicated code ======");
}
//TODO(ptab): Don't like the idea, but we have to be compatible (hope to remove the line in future release)
logger
.debug("Migrating classmap in projectData to store in *.ser file: "
+ cv.getClassMap().getClassName());
cv.getClassMap().applyOnProjectData(projectData,
cv.shouldBeInstrumented());
if (cv.shouldBeInstrumented()) {
/*
* BuildClassMapClassInstrumenter and DetectDuplicatedCodeClassVisitor has not modificated bytecode,
* so we can use any bytecode representation of that class.
*/
ClassReader cr2 = new ClassReader(cw0.toByteArray());
ClassWriter cw2 = new CoberturaClassWriter(
ClassWriter.COMPUTE_FRAMES);
cv.getClassMap().assignCounterIds();
logger.debug("Assigned " + cv.getClassMap().getMaxCounterId()
+ " counters for class:" + cv.getClassMap().getClassName());
InjectCodeClassInstrumenter cv2 = new InjectCodeClassInstrumenter(
cw2, ignoreRegexes, threadsafeRigorous, individualTest, cv.getClassMap(),
cv0.getDuplicatesLinesCollector(), detectIgnoredCv
.getIgnoredMethodNamesAndSignatures());
cr2.accept(new CheckClassAdapter(cv2), ClassReader.SKIP_FRAMES);
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
CheckClassAdapter.verify(new ClassReader(cw2.toByteArray()), false,
pw);
logger.debug(sw.toString());
return new InstrumentationResult(cv.getClassMap().getClassName(),
cw2.toByteArray());
} else {
logger.debug("Class shouldn't be instrumented: "
+ cv.getClassMap().getClassName());
return null;
}
}
/**
* Analyzes and instruments class given by file.
* <p/>
* <p>If the {@link #destinationDirectory} is null, then the file is overwritten,
* otherwise the class is stored into the {@link #destinationDirectory}</p>
* <p/>
* <p>Also the {@link #projectData} structure is filled with information about the found touch-points</p>
*
* @param file - source of class to instrument
*/
public void addInstrumentationToSingleClass(File file) {
logger.debug("Instrumenting class " + file.getAbsolutePath());
InstrumentationResult instrumentationResult = instrumentClass(file);
if (instrumentationResult != null) {
OutputStream outputStream = null;
try {
// If destinationDirectory is null, then overwrite
// the original, uninstrumented file.
File outputFile = (destinationDirectory == null)
? file
: new File(destinationDirectory,
instrumentationResult.className.replace('.',
File.separatorChar)
+ ".class");
logger.debug("Writing instrumented class into:"
+ outputFile.getAbsolutePath());
File parentFile = outputFile.getParentFile();
if (parentFile != null) {
parentFile.mkdirs();
}
outputStream = new FileOutputStream(outputFile);
outputStream.write(instrumentationResult.content);
} catch (Throwable t) {
logger.warn("Unable to write instrumented file "
+ file.getAbsolutePath(), t);
return;
} finally {
IOUtil.closeOutputStream(outputStream);
}
}
}
// ----------------- Getters and setters -------------------------------------
/**
* Gets the root directory for instrumented classes. If it is null, the instrumented classes are overwritten.
*/
public File getDestinationDirectory() {
return destinationDirectory;
}
/**
* Sets the root directory for instrumented classes. If it is null, the instrumented classes are overwritten.
*/
public void setDestinationDirectory(File destinationDirectory) {
this.destinationDirectory = destinationDirectory;
}
/**
* Gets list of patterns to know that we don't want trace lines that are calls to some methods
*/
public Collection<Pattern> getIgnoreRegexes() {
return ignoreRegexes;
}
/**
* Sets list of patterns to know that we don't want trace lines that are calls to some methods
*/
public void setIgnoreRegexes(Collection<Pattern> ignoreRegexes) {
this.ignoreRegexes = ignoreRegexes;
}
public void setIgnoreTrivial(boolean ignoreTrivial) {
this.ignoreTrivial = ignoreTrivial;
}
public void setIgnoreMethodAnnotations(Set<String> ignoreMethodAnnotations) {
this.ignoreMethodAnnotations = ignoreMethodAnnotations;
}
public void setThreadsafeRigorous(boolean threadsafeRigorous) {
this.threadsafeRigorous = threadsafeRigorous;
}
public void setFailOnError(boolean failOnError) {
this.failOnError = failOnError;
}
public void setIndividualTest(boolean individualTest) {
this.individualTest = individualTest;
}
/**
* Sets {@link ProjectData} that will be filled with information about touch points inside instrumented classes
*
* @param projectData
*/
public void setProjectData(ProjectData projectData) {
this.projectData = projectData;
}
public void setTestUnitFilePath(List<File> testUnitFilePath) {
this.testUnitFilePath = testUnitFilePath;
}
/**
* Result of instrumentation is a pair of two fields:
* <ul>
* <li> {@link #content} - bytecode of the instrumented class
* <li> {@link #className} - className of class being instrumented
* </ul>
*/
public static class InstrumentationResult {
private String className;
private byte[] content;
public InstrumentationResult(String className, byte[] content) {
this.className = className;
this.content = content;
}
public String getClassName() {
return className;
}
public byte[] getContent() {
return content;
}
}
}