001: /*
002: * @(#)GroboInstrumentTask.java
003: *
004: * Copyright (C) 2004 Matt Albrecht
005: * groboclown@users.sourceforge.net
006: * http://groboutils.sourceforge.net
007: *
008: * Permission is hereby granted, free of charge, to any person obtaining a
009: * copy of this software and associated documentation files (the "Software"),
010: * to deal in the Software without restriction, including without limitation
011: * the rights to use, copy, modify, merge, publish, distribute, sublicense,
012: * and/or sell copies of the Software, and to permit persons to whom the
013: * Software is furnished to do so, subject to the following conditions:
014: *
015: * The above copyright notice and this permission notice shall be included in
016: * all copies or substantial portions of the Software.
017: *
018: * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
019: * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
020: * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
021: * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
022: * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
023: * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
024: * DEALINGS IN THE SOFTWARE.
025: */
026:
027: package net.sourceforge.groboutils.codecoverage.v2.ant;
028:
029: import java.io.File;
030: import java.io.FileInputStream;
031: import java.io.FileOutputStream;
032: import java.io.IOException;
033: import java.util.Enumeration;
034: import java.util.Hashtable;
035: import java.util.Properties;
036: import java.util.Vector;
037:
038: import net.sourceforge.groboutils.codecoverage.v2.IAnalysisModule;
039: import net.sourceforge.groboutils.codecoverage.v2.compiler.AlreadyPostCompiledException;
040: import net.sourceforge.groboutils.codecoverage.v2.compiler.PostCompileClass;
041: import net.sourceforge.groboutils.codecoverage.v2.datastore.DirMetaDataWriter;
042: import net.sourceforge.groboutils.codecoverage.v2.logger.CacheDirChannelLoggerFactory;
043: import net.sourceforge.groboutils.codecoverage.v2.logger.DirectoryChannelLoggerFactory;
044: import net.sourceforge.groboutils.codecoverage.v2.logger.MinDirChannelLoggerFactory;
045: import net.sourceforge.groboutils.codecoverage.v2.logger.FileSingleSourceLoggerFactory;
046: import net.sourceforge.groboutils.codecoverage.v2.logger.NoOpChannelLoggerFactory;
047: import net.sourceforge.groboutils.util.io.v1.ReadByteStream;
048:
049: import org.apache.tools.ant.BuildException;
050: import org.apache.tools.ant.DirectoryScanner;
051: import org.apache.tools.ant.Project;
052: import org.apache.tools.ant.Task;
053: import org.apache.tools.ant.taskdefs.Delete;
054: import org.apache.tools.ant.types.EnumeratedAttribute;
055: import org.apache.tools.ant.types.FileSet;
056: import org.apache.tools.ant.util.FileUtils;
057:
058: /**
059: * A variation of the CoveragePostCompilerTask. This one is intended to
060: * simplify the Ant build files. See
061: * <a href="https://sourceforge.net/tracker/index.php?func=detail&aid=901588&group_id=22594&atid=375592">
062: * feature request 901588</a> for details.
063: *
064: * @author Matt Albrecht <a href="mailto:groboclown@users.sourceforge.net">groboclown@users.sourceforge.net</a>
065: * @version $Date: 2004/04/17 08:24:39 $
066: * @since March 9, 2004
067: */
068: public class GroboInstrumentTask extends Task {
069: private static final FileUtils FILEUTILS = FileUtils.newFileUtils();
070:
071: private static final String CLASSNAME_EXT = ".class";
072:
073: private static final String LOGGER_SAFE_1 = "safe";
074: private static final String LOGGER_SAFE_2 = "dir";
075: private static final String LOGGER_SAFE_3 = "directory";
076: private static final String LOGGER_SAFE_CLASS = DirectoryChannelLoggerFactory.class
077: .getName();
078: private static final String LOGGER_KEEP_OPEN_1 = "cache";
079: private static final String LOGGER_KEEP_OPEN_2 = "cachedir";
080: private static final String LOGGER_KEEP_OPEN_CLASS = CacheDirChannelLoggerFactory.class
081: .getName();
082: private static final String LOGGER_MINDIR_1 = "fast";
083: private static final String LOGGER_MINDIR_2 = "min";
084: private static final String LOGGER_MINDIR_3 = "mindir";
085: private static final String LOGGER_MINDIR_CLASS = MinDirChannelLoggerFactory.class
086: .getName();
087: private static final String LOGGER_SINGLEFILE_1 = "single file";
088: private static final String LOGGER_SINGLEFILE_2 = "single";
089: private static final String LOGGER_SINGLEFILE_CLASS = FileSingleSourceLoggerFactory.class
090: .getName();
091: private static final String LOGGER_NONE_1 = "none";
092: private static final String LOGGER_NONE_CLASS = NoOpChannelLoggerFactory.class
093: .getName();
094:
095: private static final Hashtable LOGGER_TO_CLASSNAME = new Hashtable();
096: static {
097: LOGGER_TO_CLASSNAME.put(LOGGER_SAFE_1, LOGGER_SAFE_CLASS);
098: LOGGER_TO_CLASSNAME.put(LOGGER_SAFE_2, LOGGER_SAFE_CLASS);
099: LOGGER_TO_CLASSNAME.put(LOGGER_SAFE_3, LOGGER_SAFE_CLASS);
100: LOGGER_TO_CLASSNAME.put(LOGGER_KEEP_OPEN_1,
101: LOGGER_KEEP_OPEN_CLASS);
102: LOGGER_TO_CLASSNAME.put(LOGGER_KEEP_OPEN_2,
103: LOGGER_KEEP_OPEN_CLASS);
104: LOGGER_TO_CLASSNAME.put(LOGGER_MINDIR_1, LOGGER_MINDIR_CLASS);
105: LOGGER_TO_CLASSNAME.put(LOGGER_MINDIR_2, LOGGER_MINDIR_CLASS);
106: LOGGER_TO_CLASSNAME.put(LOGGER_MINDIR_3, LOGGER_MINDIR_CLASS);
107: LOGGER_TO_CLASSNAME.put(LOGGER_SINGLEFILE_1,
108: LOGGER_SINGLEFILE_CLASS);
109: LOGGER_TO_CLASSNAME.put(LOGGER_SINGLEFILE_2,
110: LOGGER_SINGLEFILE_CLASS);
111: LOGGER_TO_CLASSNAME.put(LOGGER_NONE_1, LOGGER_NONE_CLASS);
112: }
113:
114: /**
115: * Contains all possible logger types.
116: */
117: public static final class LoggerAttribute extends
118: EnumeratedAttribute {
119: private String types[] = { LOGGER_SAFE_1, LOGGER_SAFE_2,
120: LOGGER_SAFE_3, LOGGER_KEEP_OPEN_1, LOGGER_KEEP_OPEN_2,
121: LOGGER_MINDIR_1, LOGGER_MINDIR_2, LOGGER_MINDIR_3,
122: LOGGER_SINGLEFILE_1, LOGGER_SINGLEFILE_2, LOGGER_NONE_1 };
123:
124: public String[] getValues() {
125: return this .types;
126: }
127: }
128:
129: /**
130: * Used for associating a key with a value for the properties.
131: */
132: public static final class LoggerProperty {
133: String key;
134: String value;
135:
136: public void setKey(String k) {
137: this .key = k;
138: }
139:
140: public void setValue(String v) {
141: this .value = v;
142: }
143:
144: public void setLocation(File f) {
145: this .value = f.getAbsolutePath();
146: }
147: }
148:
149: private static final String HANDLEEXISTING_REPLACE = "replace";
150: private static final String HANDLEEXISTING_KEEP = "keep";
151: private static final String HANDLEEXISTING_REMOVE_ALL = "clean";
152:
153: /**
154: * Contains all possible HandleExisting types.
155: */
156: public static final class HandleExistingAttribute extends
157: EnumeratedAttribute {
158: private String types[] = { HANDLEEXISTING_REPLACE,
159: HANDLEEXISTING_KEEP, HANDLEEXISTING_REMOVE_ALL };
160:
161: public String[] getValues() {
162: return types;
163: }
164: }
165:
166: private Vector filesets = new Vector();
167: private Vector loggerProps = new Vector();
168: private File datadir = null;
169: private File logdir = null;
170: private File outfiledir = null;
171: private File baselogdir = null;
172: private String logger = LOGGER_SAFE_1;
173: private String loggerClass = null;
174: private Vector analysisModules = new Vector();
175: private String handleExisting = HANDLEEXISTING_REPLACE;
176:
177: /**
178: * Add a new fileset instance to this compilation. Whatever the fileset is,
179: * only filename that are <tt>.class</tt> will be considered as
180: * 'candidates'. Currently, jar files are not read; you'll have to
181: * uncompress them to a directory before running this step.
182: *
183: * @param fs the new fileset containing the rules to get the testcases.
184: */
185: public void addFileSet(FileSet fs) {
186: this .filesets.addElement(fs);
187: }
188:
189: /**
190: * Set the type of logger to use. This defaults to the "safe"
191: * logger, which is JDK agnostic.
192: */
193: public void setLogger(LoggerAttribute la) {
194: this .logger = la.getValue();
195: }
196:
197: /**
198: * Allow the user to specify a logger factory class name. If this
199: * is specified, it overrides any value set by the "logger" attribute.
200: */
201: public void setLoggerFactory(String name) {
202: if (name.indexOf(".") < 0) {
203: this .loggerClass = "net.sourceforge.groboutils.codecoverage.v2.logger."
204: + name;
205: } else {
206: this .loggerClass = name;
207: }
208: }
209:
210: /**
211: * Sets the directory in which all the data accumulated from the
212: * post compilation step will be placed, and the logging output
213: * as well. This should be a directory dedicated just to the output data.
214: * If the directory doesn't exist when the task runs, it will be
215: * created.
216: */
217: public void setLogDir(File f) {
218: this .baselogdir = f;
219: }
220:
221: /**
222: * Sets the directory in which all the recompiled class files will be
223: * placed. This directory should never be confused with the original
224: * class file location.
225: */
226: public void setDestDir(File f) {
227: this .outfiledir = f;
228: }
229:
230: /**
231: * Creates a new analysis module.
232: */
233: public void addMeasure(AnalysisModuleType amt) {
234: this .analysisModules.addElement(amt);
235: }
236:
237: /**
238: * Adds a property to the logger properties file.
239: */
240: public void addLoggerProp(LoggerProperty lp) {
241: this .loggerProps.addElement(lp);
242: }
243:
244: /**
245: * Sets the behavior when classes get post-compiled - should the
246: * previous post-compiled class be replaced, kept, or should all
247: * the previous data be cleaned?
248: */
249: public void setIfExists(HandleExistingAttribute hea) {
250: this .handleExisting = hea.getValue();
251: }
252:
253: /**
254: * Perform the task
255: */
256: public void execute() throws BuildException {
257: // pre-check
258: setupDirectories();
259:
260: ClassFile classFiles[] = getFilenames();
261: IAnalysisModule modules[] = getAnalysisModules();
262:
263: try {
264: log("Writing meta-data to directory '" + this .datadir
265: + "'.", Project.MSG_VERBOSE);
266: DirMetaDataWriter dmdw = new DirMetaDataWriter(this .datadir);
267: try {
268: PostCompileClass pcc = new PostCompileClass(dmdw,
269: modules);
270: for (int i = 0; i < classFiles.length; ++i) {
271: if (HANDLEEXISTING_REPLACE
272: .equals(this .handleExisting)) {
273: cleanupClass(classFiles[i], modules);
274: }
275:
276: File infile = classFiles[i].srcFile;
277: String filename = classFiles[i].filename;
278:
279: // create the output class file, and ensure that
280: // its directory structure exists before creating it
281: File outfile = new File(this .outfiledir, filename);
282: log("Recompiling class '" + infile + "' to file '"
283: + outfile + "'.", Project.MSG_VERBOSE);
284: File parent = outfile.getParentFile();
285: if (!parent.exists()) {
286: parent.mkdirs();
287: }
288:
289: // need some code handle the situation where the
290: // outfile may be the same as the infile. This will
291: // also allow us to correctly handle the situation of
292: // an exception not properly creating the instrumented
293: // class. See bug 929332.
294: File tmpout = FILEUTILS.createTempFile(outfile
295: .getName(), ".tmp", parent);
296: FileOutputStream fos = new FileOutputStream(tmpout);
297:
298: try {
299: pcc
300: .postCompile(filename,
301: readFile(infile), fos);
302: fos.close();
303: fos = null;
304: FILEUTILS.copyFile(tmpout, outfile);
305: } catch (AlreadyPostCompiledException apce) {
306: // see bug 903837
307: log("Ignoring '" + infile
308: + "': it has already been "
309: + "post-compiled.", Project.MSG_INFO);
310: } finally {
311: if (fos != null) {
312: fos.close();
313: }
314: if (tmpout.exists()) {
315: tmpout.delete();
316: }
317: }
318: }
319: } finally {
320: dmdw.close();
321: }
322: } catch (IOException ioe) {
323: throw new BuildException("I/O exception during execution.",
324: ioe, getLocation());
325: }
326:
327: try {
328: generatePropertyFile(this .outfiledir, modules.length);
329: } catch (IOException ioe) {
330: throw new BuildException("I/O exception during execution.",
331: ioe, getLocation());
332: }
333: }
334:
335: private void setupDirectories() throws BuildException {
336: if (this .baselogdir == null) {
337: throw new BuildException(
338: "Attribute 'logdir' was never set.");
339: }
340: if (this .outfiledir == null) {
341: throw new BuildException(
342: "Attribute 'destdir' was never set.");
343: }
344:
345: if (this .datadir == null) {
346: this .datadir = new File(this .baselogdir, "data");
347: }
348: if (this .logdir == null) {
349: this .logdir = new File(this .baselogdir, "logs");
350: }
351:
352: // bug 906316: ensure the directories exist...
353: if (!this .datadir.exists()) {
354: this .datadir.mkdirs();
355: } else if (HANDLEEXISTING_REMOVE_ALL
356: .equals(this .handleExisting)) {
357: removeDir(this .datadir);
358: this .datadir.mkdirs();
359:
360: removeDir(this .logdir);
361: }
362:
363: if (!this .outfiledir.exists()) {
364: this .outfiledir.mkdirs();
365: }
366: }
367:
368: /**
369: *
370: */
371: private IAnalysisModule[] getAnalysisModules()
372: throws BuildException
373: {
374: final Vector v = new Vector();
375: final Enumeration enum = this .analysisModules.elements();
376: while (enum.hasMoreElements())
377: {
378: AnalysisModuleType amt = (AnalysisModuleType)enum.nextElement();
379: IAnalysisModule am = amt.getAnalysisModule();
380: v.addElement( am );
381: }
382: final IAnalysisModule[] amL = new IAnalysisModule[ v.size() ];
383: v.copyInto( amL );
384: return amL;
385: }
386:
387: /**
388: * Iterate over all filesets and return the filename of all files
389: * that end with <tt>.class</tt> (case insensitive). This is to avoid
390: * trying to parse a non-class file.
391: *
392: * @return an array of filenames to parse.
393: */
394: private ClassFile[] getFilenames() {
395: Vector v = new Vector();
396: final int size = this .filesets.size();
397: for (int j = 0; j < size; j++) {
398: FileSet fs = (FileSet) filesets.elementAt(j);
399: DirectoryScanner ds = fs.getDirectoryScanner(getProject());
400: File baseDir = ds.getBasedir();
401: ds.scan();
402: String[] f = ds.getIncludedFiles();
403: for (int k = 0; k < f.length; k++) {
404: String pathname = f[k];
405: if (pathname.toLowerCase().endsWith(CLASSNAME_EXT)) {
406: // this isn't right
407: v.addElement(new ClassFile(baseDir, pathname));
408: }
409: }
410: }
411:
412: ClassFile[] files = new ClassFile[v.size()];
413: v.copyInto(files);
414: return files;
415: }
416:
417: /**
418: * Contains the data for the class file.
419: */
420: private static final class ClassFile {
421: public File srcFile;
422: public String filename;
423:
424: public ClassFile(File baseDir, String filename) {
425: if (baseDir == null || filename == null) {
426: throw new IllegalArgumentException("no null args.");
427: }
428: this .filename = filename;
429: this .srcFile = new File(baseDir, filename);
430: }
431: }
432:
433: /**
434: * Create the property file for the logger.
435: */
436: private void generatePropertyFile( File outfiledir, int moduleCount )
437: throws IOException
438: {
439: Properties props = new Properties();
440: if (this .loggerClass != null)
441: {
442: props.setProperty( "factory", this .loggerClass );
443: }
444: else
445: {
446: String factoryClass = (String)LOGGER_TO_CLASSNAME.get(
447: this .logger );
448: if (LOGGER_NONE_CLASS.equals( factoryClass ))
449: {
450: // feature: specifying a logger type (not class) of
451: // none means don't create the property file.
452: return;
453: }
454: props.setProperty( "factory", factoryClass );
455: }
456:
457: if (this .logdir != null)
458: {
459: props.setProperty( "logger.dir",
460: this .logdir.getAbsolutePath() );
461: }
462:
463: Enumeration enum = loggerProps.elements();
464: while (enum.hasMoreElements())
465: {
466: LoggerProperty lp = (LoggerProperty)enum.nextElement();
467: if (lp.key == null)
468: {
469: throw new BuildException( "No key given for loggerprop." );
470: }
471: if (lp.value == null)
472: {
473: throw new BuildException(
474: "No value or location given for loggerprop key \""
475: + lp.key + "\"." );
476: }
477: props.setProperty( "logger." + lp.key, lp.value );
478: }
479:
480: props.setProperty( "channel-count",
481: Integer.toString( moduleCount ) );
482:
483: FileOutputStream fos = new FileOutputStream(
484: new File( outfiledir, "grobocoverage.properties" ) );
485: try
486: {
487: props.store( fos, "CodeCoverage setup file" );
488: }
489: finally
490: {
491: fos.close();
492: }
493: }
494:
495: /**
496: *
497: */
498: private byte[] readFile(File file) throws IOException {
499: FileInputStream fis = new FileInputStream(file);
500: try {
501: byte[] outfile = ReadByteStream.readByteStream(fis);
502: return outfile;
503: } finally {
504: fis.close();
505: }
506: }
507:
508: /**
509: * Recursively removes a directory's contents
510: */
511: private void removeDir(File d) {
512: MyDelete md = new MyDelete();
513: md.setProject(getProject());
514: md.setFailOnError(false);
515: md.removeDir2(d);
516: }
517:
518: /** all we care about is making the removeDirectory public */
519: private static final class MyDelete extends Delete {
520: // save some execution time by not overriding removeDir, but
521: // instead have a different method that calls out to the
522: // protected method.
523: public void removeDir2(File f) {
524: if (f != null && f.exists()) {
525: if (f.isDirectory()) {
526: super .removeDir(f);
527: } else {
528: f.delete();
529: }
530: }
531: }
532: }
533:
534: /**
535: * Cleanup the datafiles for the given class. The implementation
536: * is rather hacky.
537: */
538: private void cleanupClass(ClassFile cf, IAnalysisModule[] modules)
539: throws IOException {
540: String cfName = cf.filename.replace(File.separatorChar, '.');
541: if (cfName.toLowerCase().endsWith(CLASSNAME_EXT)) {
542: cfName = cfName.substring(0, cfName.length()
543: - CLASSNAME_EXT.length());
544: }
545: // be sure to add in the last '-', otherwise inner-classes and
546: // other classes that start with the same text may be accidentally
547: // deleted. This was happening in the testInstrument5 test.
548: cfName = cfName + "-";
549:
550: for (int amIndex = 0; amIndex < modules.length; ++amIndex) {
551: // analysis module data dir
552: File amdd = new File(this .datadir, modules[amIndex]
553: .getMeasureName());
554: File list[] = amdd.listFiles();
555: if (list != null) {
556: for (int i = 0; i < list.length; ++i) {
557: // names are of the form:
558: // [full class name]-[CRC].[type].txt
559: String name = list[i].getName();
560: if (name.startsWith(cfName)) {
561: // delete all for this class!!!
562: list[i].delete();
563: }
564: }
565: }
566: }
567: }
568: }
|