001/* 002 * Cobertura - http://cobertura.sourceforge.net/ 003 * 004 * Copyright (C) 2003 jcoverage ltd. 005 * Copyright (C) 2005 Mark Doliner 006 * Copyright (C) 2005 Joakim Erdfelt 007 * Copyright (C) 2005 Grzegorz Lukasik 008 * Copyright (C) 2006 John Lewis 009 * Copyright (C) 2006 Jiri Mares 010 * Copyright (C) 2008 Scott Frederick 011 * Copyright (C) 2010 Tad Smith 012 * Copyright (C) 2010 Piotr Tabor 013 * Contact information for the above is given in the COPYRIGHT file. 014 * 015 * Cobertura is free software; you can redistribute it and/or modify 016 * it under the terms of the GNU General Public License as published 017 * by the Free Software Foundation; either version 2 of the License, 018 * or (at your option) any later version. 019 * 020 * Cobertura is distributed in the hope that it will be useful, but 021 * WITHOUT ANY WARRANTY; without even the implied warranty of 022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 023 * General Public License for more details. 024 * 025 * You should have received a copy of the GNU General Public License 026 * along with Cobertura; if not, write to the Free Software 027 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 028 * USA 029 */ 030 031package net.sourceforge.cobertura.instrument; 032 033import java.io.ByteArrayInputStream; 034import java.io.ByteArrayOutputStream; 035import java.io.File; 036import java.io.FileInputStream; 037import java.io.FileNotFoundException; 038import java.io.FileOutputStream; 039import java.io.IOException; 040import java.io.InputStream; 041import java.io.OutputStream; 042import java.util.ArrayList; 043import java.util.Collection; 044import java.util.HashSet; 045import java.util.Iterator; 046import java.util.List; 047import java.util.Set; 048import java.util.Vector; 049import java.util.regex.Pattern; 050import java.util.zip.ZipEntry; 051import java.util.zip.ZipInputStream; 052import java.util.zip.ZipOutputStream; 053 054import net.sourceforge.cobertura.coveragedata.CoverageDataFileHandler; 055import net.sourceforge.cobertura.coveragedata.ProjectData; 056import net.sourceforge.cobertura.instrument.CoberturaInstrumenter.InstrumentationResult; 057import net.sourceforge.cobertura.util.ArchiveUtil; 058import net.sourceforge.cobertura.util.CommandLineBuilder; 059import net.sourceforge.cobertura.util.Header; 060import net.sourceforge.cobertura.util.IOUtil; 061import net.sourceforge.cobertura.util.RegexUtil; 062 063import org.apache.log4j.Logger; 064 065/** 066 * <p> 067 * Add coverage instrumentation to existing classes. 068 * </p> 069 * 070 * <h3>What does that mean, exactly?</h3> 071 * <p> 072 * It means Cobertura will look at each class you give it. It 073 * loads the bytecode into memory. For each line of source, 074 * Cobertura adds a few extra instructions. These instructions 075 * do the following: 076 * </p> 077 * 078 * <ol> 079 * <li>Get an instance of the ProjectData class.</li> 080 * <li>Call a method in this ProjectData class that increments 081 * a counter for this line of code. 082 * </ol> 083 */ 084public class Main { 085 private static final LoggerWrapper logger = new LoggerWrapper(); 086 087 private File destinationDirectory = null; 088 089 private final ClassPattern classPattern = new ClassPattern(); 090 091 private final CoberturaInstrumenter coberturaInstrumenter = new CoberturaInstrumenter(); 092 093 /** 094 * @param entry A zip entry. 095 * @return True if the specified entry has "class" as its extension, 096 * false otherwise. 097 */ 098 private static boolean isClass(ZipEntry entry) 099 { 100 return entry.getName().endsWith(".class"); 101 } 102 103 private boolean addInstrumentationToArchive(CoberturaFile file, InputStream archive, 104 OutputStream output) throws Exception 105 { 106 ZipInputStream zis = null; 107 ZipOutputStream zos = null; 108 109 try 110 { 111 zis = new ZipInputStream(archive); 112 zos = new ZipOutputStream(output); 113 return addInstrumentationToArchive(file, zis, zos); 114 } 115 finally 116 { 117 zis = (ZipInputStream)IOUtil.closeInputStream(zis); 118 zos = (ZipOutputStream)IOUtil.closeOutputStream(zos); 119 } 120 } 121 122 private boolean addInstrumentationToArchive(CoberturaFile file, ZipInputStream archive, 123 ZipOutputStream output) throws Exception 124 { 125 /* 126 * "modified" is returned and indicates that something was instrumented. 127 * If nothing is instrumented, the original entry will be used by the 128 * caller of this method. 129 */ 130 boolean modified = false; 131 ZipEntry entry; 132 while ((entry = archive.getNextEntry()) != null) 133 { 134 try 135 { 136 String entryName = entry.getName(); 137 138 /* 139 * If this is a signature file then don't copy it, 140 * but don't set modified to true. If the only 141 * thing we do is strip the signature, just use 142 * the original entry. 143 */ 144 if (ArchiveUtil.isSignatureFile(entry.getName())) 145 { 146 continue; 147 } 148 ZipEntry outputEntry = new ZipEntry(entry.getName()); 149 outputEntry.setComment(entry.getComment()); 150 outputEntry.setExtra(entry.getExtra()); 151 outputEntry.setTime(entry.getTime()); 152 output.putNextEntry(outputEntry); 153 154 // Read current entry 155 byte[] entryBytes = IOUtil 156 .createByteArrayFromInputStream(archive); 157 158 // Instrument embedded archives if a classPattern has been specified 159 if ((classPattern.isSpecified()) && ArchiveUtil.isArchive(entryName)) 160 { 161 Archive archiveObj = new Archive(file, entryBytes); 162 addInstrumentationToArchive(archiveObj); 163 if (archiveObj.isModified()) 164 { 165 modified = true; 166 entryBytes = archiveObj.getBytes(); 167 outputEntry.setTime(System.currentTimeMillis()); 168 } 169 } 170 else if (isClass(entry) && classPattern.matches(entryName)) 171 { 172 try { 173 InstrumentationResult res=coberturaInstrumenter.instrumentClass(new ByteArrayInputStream(entryBytes)); 174 if(res!=null){ 175 logger.debug("Putting instrumented entry: " + entry.getName()); 176 entryBytes=res.getContent(); 177 modified=true; 178 outputEntry.setTime(System.currentTimeMillis()); 179 } 180 } 181 catch (Throwable t) 182 { 183 if (entry.getName().endsWith("_Stub.class")) 184 { 185 //no big deal - it is probably an RMI stub, and they don't need to be instrumented 186 logger.debug("Problems instrumenting archive entry: " + entry.getName(), t); 187 } 188 else 189 { 190 logger.warn("Problems instrumenting archive entry: " + entry.getName(), t); 191 } 192 } 193 } 194 195 // Add entry to the output 196 output.write(entryBytes); 197 output.closeEntry(); 198 archive.closeEntry(); 199 } 200 catch (Exception e) 201 { 202 logger.warn("Problems with archive entry: " + entry.getName(), e); 203 } 204 catch (Throwable t) 205 { 206 logger.warn("Problems with archive entry: " + entry.getName(), t); 207 } 208 output.flush(); 209 } 210 return modified; 211 } 212 213 private void addInstrumentationToArchive(Archive archive) throws Exception 214 { 215 InputStream in = null; 216 ByteArrayOutputStream out = null; 217 try 218 { 219 in = archive.getInputStream(); 220 out = new ByteArrayOutputStream(); 221 boolean modified = addInstrumentationToArchive(archive.getCoberturaFile(), in, out); 222 223 if (modified) 224 { 225 out.flush(); 226 byte[] bytes = out.toByteArray(); 227 archive.setModifiedBytes(bytes); 228 } 229 } 230 finally 231 { 232 in = IOUtil.closeInputStream(in); 233 out = (ByteArrayOutputStream)IOUtil.closeOutputStream(out); 234 } 235 } 236 237 private void addInstrumentationToArchive(CoberturaFile archive) 238 { 239 logger.debug("Instrumenting archive " + archive.getAbsolutePath()); 240 241 File outputFile = null; 242 ZipInputStream input = null; 243 ZipOutputStream output = null; 244 boolean modified = false; 245 try 246 { 247 // Open archive 248 try 249 { 250 input = new ZipInputStream(new FileInputStream(archive)); 251 } 252 catch (FileNotFoundException e) 253 { 254 logger.warn("Cannot open archive file: " 255 + archive.getAbsolutePath(), e); 256 return; 257 } 258 259 // Open output archive 260 try 261 { 262 // check if destination folder is set 263 if (destinationDirectory != null) 264 { 265 // if so, create output file in it 266 outputFile = new File(destinationDirectory, archive.getPathname()); 267 } 268 else 269 { 270 // otherwise create output file in temporary location 271 outputFile = File.createTempFile( 272 "CoberturaInstrumentedArchive", "jar"); 273 outputFile.deleteOnExit(); 274 } 275 output = new ZipOutputStream(new FileOutputStream(outputFile)); 276 } 277 catch (IOException e) 278 { 279 logger.warn("Cannot open file for instrumented archive: " 280 + archive.getAbsolutePath(), e); 281 return; 282 } 283 284 // Instrument classes in archive 285 try 286 { 287 modified = addInstrumentationToArchive(archive, input, output); 288 } 289 catch (Throwable e) 290 { 291 logger.warn("Cannot instrument archive: " 292 + archive.getAbsolutePath(), e); 293 return; 294 } 295 } 296 finally 297 { 298 input = (ZipInputStream)IOUtil.closeInputStream(input); 299 output = (ZipOutputStream)IOUtil.closeOutputStream(output); 300 } 301 302 // If destination folder was not set, overwrite orginal archive with 303 // instrumented one 304 if (modified && (destinationDirectory == null)) 305 { 306 try 307 { 308 logger.debug("Moving " + outputFile.getAbsolutePath() + " to " 309 + archive.getAbsolutePath()); 310 IOUtil.moveFile(outputFile, archive); 311 } 312 catch (IOException e) 313 { 314 logger.warn("Cannot instrument archive: " 315 + archive.getAbsolutePath(), e); 316 return; 317 } 318 } 319 if ((destinationDirectory != null) && (!modified)) 320 { 321 outputFile.delete(); 322 } 323 } 324 325 326 327 private void addInstrumentationToSingleClass(File file){ 328 logger.info("Instrumenting: "+file.getAbsolutePath()+" to "+destinationDirectory); 329 coberturaInstrumenter.addInstrumentationToSingleClass(file); 330 } 331 332 // TODO: Don't attempt to instrument a file if the outputFile already 333 // exists and is newer than the input file, and the output and 334 // input file are in different locations? 335 private void addInstrumentation(CoberturaFile coberturaFile) 336 { 337 if (coberturaFile.isClass() && classPattern.matches(coberturaFile.getPathname())) 338 { 339 addInstrumentationToSingleClass(coberturaFile); 340 } 341 else if (coberturaFile.isDirectory()) 342 { 343 String[] contents = coberturaFile.list(); 344 for (int i = 0; i < contents.length; i++) 345 { 346 File relativeFile = new File(coberturaFile.getPathname(), contents[i]); 347 CoberturaFile relativeCoberturaFile = new CoberturaFile(coberturaFile.getBaseDir(), 348 relativeFile.toString()); 349 //recursion! 350 addInstrumentation(relativeCoberturaFile); 351 } 352 } 353 } 354 355 private void parseArguments(String[] args){ 356 Collection<Pattern> ignoreRegexes = new Vector<Pattern>(); 357 coberturaInstrumenter.setIgnoreRegexes(ignoreRegexes); 358 359 File dataFile = CoverageDataFileHandler.getDefaultDataFile(); 360 361 // Parse our parameters 362 List<CoberturaFile> filePaths = new ArrayList<CoberturaFile>(); 363 String baseDir = null; 364 365 boolean threadsafeRigorous = false; 366 boolean ignoreTrivial = false; 367 boolean failOnError = false; 368 Set<String> ignoreMethodAnnotations = new HashSet<String>(); 369 370 for (int i = 0; i < args.length; i++) 371 { 372 if (args[i].equals("--basedir")) 373 baseDir = args[++i]; 374 else if (args[i].equals("--datafile")) 375 dataFile = new File(args[++i]); 376 else if (args[i].equals("--destination")) { 377 destinationDirectory = new File(args[++i]); 378 coberturaInstrumenter.setDestinationDirectory(destinationDirectory); 379 } else if (args[i].equals("--ignore")) { 380 RegexUtil.addRegex(ignoreRegexes, args[++i]); 381 } 382 /*else if (args[i].equals("--ignoreBranches")) 383 { 384 RegexUtil.addRegex(ignoreBranchesRegexes, args[++i]); 385 }*/ 386 else if (args[i].equals("--ignoreMethodAnnotation")) { 387 ignoreMethodAnnotations.add(args[++i]); 388 } else if (args[i].equals("--ignoreTrivial")) { 389 ignoreTrivial = true; 390 } else if (args[i].equals("--includeClasses")) { 391 classPattern.addIncludeClassesRegex(args[++i]); 392 } else if (args[i].equals("--excludeClasses")) { 393 classPattern.addExcludeClassesRegex(args[++i]); 394 } else if (args[i].equals("--failOnError")) { 395 failOnError = true; 396 logger.setFailOnError(true); 397 } else if (args[i].equals("--threadsafeRigorous")) { 398 threadsafeRigorous = true; 399 } else { 400 filePaths.add(new CoberturaFile(baseDir, args[i])); 401 } 402 } 403 404 coberturaInstrumenter.setIgnoreTrivial(ignoreTrivial); 405 coberturaInstrumenter.setIgnoreMethodAnnotations(ignoreMethodAnnotations); 406 coberturaInstrumenter.setThreadsafeRigorous(threadsafeRigorous); 407 coberturaInstrumenter.setFailOnError(failOnError); 408 409 ProjectData projectData; 410 411 // Load previous coverage data (if exists) 412 projectData = dataFile.isFile() ? 413 CoverageDataFileHandler.loadCoverageData(dataFile) : new ProjectData(); 414 coberturaInstrumenter.setProjectData(projectData); 415 416 // Instrument classes 417 logger.info("Instrumenting " + filePaths.size() + " " 418 + (filePaths.size() == 1 ? "file" : "files") 419 + (destinationDirectory != null ? " to " 420 + destinationDirectory.getAbsoluteFile() : "")); 421 422 Iterator<CoberturaFile> iter = filePaths.iterator(); 423 while (iter.hasNext()) { 424 CoberturaFile coberturaFile = iter.next(); 425 if (coberturaFile.isArchive()) { 426 addInstrumentationToArchive(coberturaFile); 427 } else { 428 addInstrumentation(coberturaFile); 429 } 430 } 431 432 // Save coverage data (ser file with list of touch points, but not hits registered). 433 CoverageDataFileHandler.saveCoverageData(projectData, dataFile); 434 } 435 436 public static void main(String[] args) 437 { 438 Header.print(System.out); 439 440 long startTime = System.currentTimeMillis(); 441 442 Main main = new Main(); 443 444 try { 445 args = CommandLineBuilder.preprocessCommandLineArguments( args); 446 } catch( Exception ex) { 447 System.err.println( "Error: Cannot process arguments: " + ex.getMessage()); 448 System.exit(1); 449 } 450 main.parseArguments(args); 451 452 long stopTime = System.currentTimeMillis(); 453 logger.info("Instrument time: " + (stopTime - startTime) + "ms"); 454 } 455 456 // TODO: Preserved current behaviour, but this code is failing on WARN, not error 457 private static class LoggerWrapper { 458 private final Logger logger = Logger.getLogger(Main.class); 459 private boolean failOnError = false; 460 461 public void setFailOnError(boolean failOnError){ 462 this.failOnError = failOnError; 463 } 464 465 public void debug(String message) { 466 logger.debug(message); 467 } 468 469 public void debug(String message, Throwable t){ 470 logger.debug(message, t); 471 } 472 473 public void info(String message){ 474 logger.debug(message); 475 } 476 477 public void warn(String message, Throwable t) { 478 logger.warn(message, t); 479 if (failOnError) { 480 throw new RuntimeException("Warning detected and failOnError is true", t); 481 } 482 } 483 } 484 485}