001/* 002 * Cobertura - http://cobertura.sourceforge.net/ 003 * 004 * Copyright (C) 2011 Piotr Tabor 005 * 006 * Note: This file is dual licensed under the GPL and the Apache 007 * Source License (so that it can be used from both the main 008 * Cobertura classes and the ant tasks). 009 * 010 * Cobertura is free software; you can redistribute it and/or modify 011 * it under the terms of the GNU General Public License as published 012 * by the Free Software Foundation; either version 2 of the License, 013 * or (at your option) any later version. 014 * 015 * Cobertura is distributed in the hope that it will be useful, but 016 * WITHOUT ANY WARRANTY; without even the implied warranty of 017 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 018 * General Public License for more details. 019 * 020 * You should have received a copy of the GNU General Public License 021 * along with Cobertura; if not, write to the Free Software 022 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 023 * USA 024 */ 025 026package net.sourceforge.cobertura.instrument; 027 028import java.io.File; 029import java.io.FileInputStream; 030import java.io.FileOutputStream; 031import java.io.IOException; 032import java.io.InputStream; 033import java.io.OutputStream; 034import java.util.Collection; 035import java.util.HashSet; 036import java.util.Map; 037import java.util.Set; 038import java.util.Vector; 039import java.util.regex.Pattern; 040 041import net.sourceforge.cobertura.coveragedata.ProjectData; 042import net.sourceforge.cobertura.instrument.pass1.DetectDuplicatedCodeClassVisitor; 043import net.sourceforge.cobertura.instrument.pass1.DetectIgnoredCodeClassVisitor; 044import net.sourceforge.cobertura.instrument.pass2.BuildClassMapClassVisitor; 045import net.sourceforge.cobertura.instrument.pass3.InjectCodeClassInstrumenter; 046import net.sourceforge.cobertura.util.IOUtil; 047 048import org.apache.log4j.Logger; 049import org.objectweb.asm.ClassReader; 050import org.objectweb.asm.ClassWriter; 051 052/** 053 * Class that is responsible for the whole process of instrumentation of a single class. 054 * 055 * The class is instrumented in tree passes: 056 * <ol> 057 * <li>Read only: {@link DetectDuplicatedCodeClassVisitor} - we look for the same ASM code snippets 058 * rendered in different places of destination code</li> 059 * <li>Read only: {@link BuildClassMapClassVisitor} - finds all touch-points and other interesting 060 * information that are in the class and store it in {@link ClassMap}. 061 * <li>Real instrumentation: {@link InjectCodeClassInstrumenter}. Uses {#link ClassMap} to inject 062 * code into the class</li> 063 * </ol> 064 * 065 * @author piotr.tabor@gmail.com 066 */ 067public class CoberturaInstrumenter { 068 private static final Logger logger = Logger.getLogger(CoberturaInstrumenter.class); 069 070 /** 071 * During the instrumentation process we are feeling {@link ProjectData}, to generate from 072 * it the *.ser file. 073 * 074 * We now (1.10+) don't need to generate the file (it is not necessery for reporting), but we still 075 * do it for backward compatibility (for example maven-cobertura-plugin expects it). We should avoid 076 * this some day. 077 */ 078 private ProjectData projectData; 079 080 /** 081 * The root directory for instrumented classes. If it is null, the instrumented classes are overwritten. 082 */ 083 private File destinationDirectory; 084 085 /** 086 * List of patterns to know that we don't want trace lines that are calls to some methods 087 */ 088 private Collection<Pattern> ignoreRegexes = new Vector<Pattern>(); 089 090 /** 091 * Methods annotated by this annotations will be ignored during coverage measurement 092 */ 093 private Set<String> ignoreMethodAnnotations = new HashSet<String>(); 094 095 /** 096 * If true: Getters, Setters and simple initialization will be ignored by coverage measurement 097 */ 098 private boolean ignoreTrivial; 099 100 /** 101 * If true: The process is interrupted when first error occured. 102 */ 103 private boolean failOnError; 104 105 /** 106 * Setting to true causes cobertura to use more strict threadsafe model that is significantly 107 * slower, but guarantees that number of hits counted for each line will be precise in multithread-environment. 108 * 109 * The option does not change measured coverage. 110 * 111 * In implementation it means that AtomicIntegerArray will be used instead of int[]. 112 */ 113 private boolean threadsafeRigorous; 114 115 /** 116 * Analyzes and instruments class given by path. 117 * 118 * <p>Also the {@link #projectData} structure is filled with information about the found touch-points</p> 119 * 120 * @param file - path to class that should be instrumented 121 * 122 * @return instrumentation result structure or null in case of problems 123 */ 124 public InstrumentationResult instrumentClass(File file){ 125 InputStream inputStream = null; 126 try{ 127 logger.debug("Working on file:" + file.getAbsolutePath()); 128 inputStream = new FileInputStream(file); 129 return instrumentClass(inputStream); 130 }catch (Throwable t){ 131 logger.warn("Unable to instrument file " + file.getAbsolutePath(),t); 132 if (failOnError) { 133 throw new RuntimeException("Warning detected and failOnError is true", t); 134 } else { 135 return null; 136 } 137 }finally{ 138 IOUtil.closeInputStream(inputStream); 139 } 140 } 141 142 /** 143 * Analyzes and instruments class given by inputStream 144 * 145 * <p>Also the {@link #projectData} structure is filled with information about the found touch-points</p> 146 * 147 * @param inputStream - source of class to instrument * 148 * @return instrumentation result structure or null in case of problems 149 */ 150 public InstrumentationResult instrumentClass(InputStream inputStream) throws IOException{ 151 ClassReader cr0 = new ClassReader(inputStream); 152 ClassWriter cw0 = new ClassWriter(0); 153 DetectIgnoredCodeClassVisitor detectIgnoredCv = 154 new DetectIgnoredCodeClassVisitor(cw0, ignoreTrivial, ignoreMethodAnnotations); 155 DetectDuplicatedCodeClassVisitor cv0=new DetectDuplicatedCodeClassVisitor(detectIgnoredCv); 156 cr0.accept(cv0, 0); 157 158 ClassReader cr = new ClassReader(cw0.toByteArray()); 159 ClassWriter cw = new ClassWriter(0); 160 BuildClassMapClassVisitor cv = new BuildClassMapClassVisitor(cw, ignoreRegexes,cv0.getDuplicatesLinesCollector(), 161 detectIgnoredCv.getIgnoredMethodNamesAndSignatures()); 162 163 cr.accept(cv, ClassReader.EXPAND_FRAMES); 164 165 if(logger.isDebugEnabled()){ 166 logger.debug("=============== Detected duplicated code ============="); 167 Map<Integer, Map<Integer, Integer>> l=cv0.getDuplicatesLinesCollector(); 168 for(Map.Entry<Integer, Map<Integer,Integer>> m:l.entrySet()){ 169 if (m.getValue()!=null){ 170 for(Map.Entry<Integer, Integer> pair:m.getValue().entrySet()){ 171 logger.debug(cv.getClassMap().getClassName()+":"+m.getKey()+" "+pair.getKey()+"->"+pair.getValue()); 172 } 173 } 174 } 175 logger.debug("=============== End of detected duplicated code ======"); 176 } 177 178 //TODO(ptab): Don't like the idea, but we have to be compatible (hope to remove the line in future release) 179 logger.debug("Migrating classmap in projectData to store in *.ser file: " + cv.getClassMap().getClassName()); 180 181 cv.getClassMap().applyOnProjectData(projectData, cv.shouldBeInstrumented()); 182 183 if (cv.shouldBeInstrumented()){ 184 /* 185 * BuildClassMapClassInstrumenter and DetectDuplicatedCodeClassVisitor has not modificated bytecode, 186 * so we can use any bytecode representation of that class. 187 */ 188 ClassReader cr2= new ClassReader(cw0.toByteArray()); 189 ClassWriter cw2= new ClassWriter(ClassWriter.COMPUTE_FRAMES); 190 cv.getClassMap().assignCounterIds(); 191 logger.debug("Assigned "+ cv.getClassMap().getMaxCounterId()+" counters for class:"+cv.getClassMap().getClassName()); 192 InjectCodeClassInstrumenter cv2 = new InjectCodeClassInstrumenter(cw2, ignoreRegexes, 193 threadsafeRigorous, cv.getClassMap(), cv0.getDuplicatesLinesCollector(), detectIgnoredCv.getIgnoredMethodNamesAndSignatures()); 194 cr2.accept(cv2, ClassReader.EXPAND_FRAMES); 195 return new InstrumentationResult(cv.getClassMap().getClassName(), cw2.toByteArray()); 196 }else{ 197 logger.debug("Class shouldn't be instrumented: "+cv.getClassMap().getClassName()); 198 return null; 199 } 200 } 201 202 /** 203 * Analyzes and instruments class given by file. 204 * 205 * <p>If the {@link #destinationDirectory} is null, then the file is overwritten, 206 * otherwise the class is stored into the {@link #destinationDirectory}</p> 207 * 208 * <p>Also the {@link #projectData} structure is filled with information about the found touch-points</p> 209 * 210 * @param file - source of class to instrument 211 */ 212 public void addInstrumentationToSingleClass(File file) 213 { 214 logger.debug("Instrumenting class " + file.getAbsolutePath()); 215 216 InstrumentationResult instrumentationResult=instrumentClass(file); 217 if (instrumentationResult!=null){ 218 OutputStream outputStream = null; 219 try{ 220 // If destinationDirectory is null, then overwrite 221 // the original, uninstrumented file. 222 File outputFile=(destinationDirectory == null)?file 223 :new File(destinationDirectory, instrumentationResult.className.replace('.', File.separatorChar)+ ".class"); 224 logger.debug("Writing instrumented class into:"+outputFile.getAbsolutePath()); 225 226 File parentFile = outputFile.getParentFile(); 227 if (parentFile != null){ 228 parentFile.mkdirs(); 229 } 230 231 outputStream = new FileOutputStream(outputFile); 232 outputStream.write(instrumentationResult.content); 233 }catch (Throwable t){ 234 logger.warn("Unable to write instrumented file " + file.getAbsolutePath(),t); 235 return; 236 }finally{ 237 outputStream = IOUtil.closeOutputStream(outputStream); 238 } 239 } 240 } 241 242// ----------------- Getters and setters ------------------------------------- 243 244 /** 245 * Gets the root directory for instrumented classes. If it is null, the instrumented classes are overwritten. 246 */ 247 public File getDestinationDirectory() { 248 return destinationDirectory; 249 } 250 251 /** 252 *Sets the root directory for instrumented classes. If it is null, the instrumented classes are overwritten. 253 */ 254 public void setDestinationDirectory(File destinationDirectory) { 255 this.destinationDirectory = destinationDirectory; 256 } 257 258 /** 259 * Gets list of patterns to know that we don't want trace lines that are calls to some methods 260 */ 261 public Collection<Pattern> getIgnoreRegexes() { 262 return ignoreRegexes; 263 } 264 265 /** 266 * Sets list of patterns to know that we don't want trace lines that are calls to some methods 267 */ 268 public void setIgnoreRegexes(Collection<Pattern> ignoreRegexes) { 269 this.ignoreRegexes = ignoreRegexes; 270 } 271 272 public void setIgnoreTrivial(boolean ignoreTrivial) { 273 this.ignoreTrivial = ignoreTrivial; 274 } 275 276 public void setIgnoreMethodAnnotations(Set<String> ignoreMethodAnnotations) { 277 this.ignoreMethodAnnotations = ignoreMethodAnnotations; 278 } 279 280 public void setThreadsafeRigorous(boolean threadsafeRigorous) { 281 this.threadsafeRigorous = threadsafeRigorous; 282 } 283 284 public void setFailOnError(boolean failOnError) { 285 this.failOnError = failOnError; 286 } 287 288 289 /** 290 * Sets {@link ProjectData} that will be filled with information about touch points inside instrumented classes 291 * @param projectData 292 */ 293 public void setProjectData(ProjectData projectData) { 294 this.projectData=projectData; 295 } 296 297 /** 298 * Result of instrumentation is a pair of two fields: 299 * <ul> 300 * <li> {@link #content} - bytecode of the instrumented class 301 * <li> {@link #className} - className of class being instrumented 302 * </ul> 303 */ 304 public static class InstrumentationResult{ 305 private String className; 306 private byte[] content; 307 public InstrumentationResult(String className,byte[] content) { 308 this.className=className; 309 this.content=content; 310 } 311 312 public String getClassName() { 313 return className; 314 } 315 public byte[] getContent() { 316 return content; 317 } 318 } 319}