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}