001: package csdl.jblanket.modifier;
002:
003: import csdl.jblanket.JBlanketException;
004: import csdl.jblanket.methodset.MethodInfo;
005: import csdl.jblanket.methodset.MethodSet;
006: import csdl.jblanket.methodset.MethodSetManager;
007:
008: import java.io.File;
009: import java.io.FileInputStream;
010: import java.io.FileNotFoundException;
011: import java.io.FileOutputStream;
012: import java.io.IOException;
013: import java.text.DateFormat;
014: import java.text.SimpleDateFormat;
015: import java.util.ArrayList;
016: import java.util.Date;
017: import java.util.Properties;
018:
019: import org.apache.tools.ant.DirectoryScanner;
020:
021: /**
022: * Collects the type signatures of methods invoked during JUnit testing.
023: * <p>
024: * This class is used during the second step in JBlanket -- recording the method invocations per
025: * JUnit test class with <code>storeMethodTypeSignature</code>. This method is inserted into the
026: * byte code of every non-abstract or non-native method in .class files that should be included in
027: * coverage. After every test class is executed, the results are stored in a "COVER-*.xml" file,
028: * where * is the fully qualified name of the test class. Methods invoked on the server during
029: * testing are recorded in "COVER-<class>.xml" where <class> is the fully qualified
030: * name of the first class invoked on the server.
031: * <p>
032: * To use this class, the "jblanket.dir" system property should be set to a directory in which
033: * all JBlanket results will be stored. If none is set, the default value is:
034: * <ul>
035: * System.getProperty("user.home")\jblanket.
036: * </ul>
037: * <p>
038: * All public static methods of this class are invoked by other methods at run time from
039: * <code>storeMethodTypeSignature</code> and are coded in this manner to simplify instrumentation.
040: *
041: * @author Joy M. Agustin
042: * @version $Id: MethodCollector.java,v 1.2 2005/03/08 08:02:01 timshadel Exp $id
043: */
044: public class MethodCollector {
045:
046: /** Holds the class name used to store method information */
047: private static String currentClassName;
048:
049: /** Prefix of file name for JBlanket intermediate coverage data from running JUnit tests */
050: private static final String COVER_PREFIX = "COVER-";
051: /** File extension for XML files */
052: private static final String XML_EXTENSION = ".xml";
053:
054: /** Time stamp to store in JBlanket summary output files */
055: private Date earliestDate;
056:
057: /** Separator between directories */
058: private static final String SLASH = File.separator;
059:
060: /** Empty constructor. */
061: private MethodCollector() {
062: }
063:
064: /**
065: * Stores the type signature of <code>methodName</code> whenever the modified method
066: * <code>methodName</code> is invoked. The name of the current test class is stored so that
067: * methods invoked in different test classes can be stored in different files.
068: * <p>
069: * ASSUMPTION: no test class calls the methods in another test class. Therefore, results from
070: * running test cases will be stored in separate files and not overwrite other results.
071: *
072: * @param className the fully qualified name of class containing <code>methodName</code>.
073: * @param methodName the name of the method invoked.
074: * @param paramList the list of parameter types of <code>methodName</code>.
075: * @param testGrammar the grammar describing names of test classes. The only acceptable grammars
076: * are 'Test*.class' and '*Test.class'.
077: * @throws JBlanketException if cannot find 'jblanket.dir' system property.
078: */
079: public static synchronized void storeMethodTypeSignature(
080: String className, String methodName, ArrayList paramList,
081: String testGrammar) throws JBlanketException {
082:
083: // default name of constructors
084: final String constructor = "<init>";
085:
086: // check if class is a test class
087: if ((currentClassName == null)
088: || isTestClass(className, testGrammar)) {
089: currentClassName = className;
090: }
091:
092: // get MethodSet instance for storing the method's type signature
093: MethodSet methodSet = MethodSetManager.getInstance()
094: .getMethodSet(currentClassName);
095: ArrayList reconstructedList = new ArrayList();
096:
097: // check parameter types before adding them to methodInfo object
098: for (int i = 0; i < paramList.size(); i++) {
099: reconstructedList.add(reconstructType((String) paramList
100: .get(i)));
101: }
102:
103: // reconstruct names of constructors from default name '<init>'
104: if (constructor.equals(methodName)) {
105: methodName = removePackagePrefix(className);
106: }
107: if ("<clinit>".equals(methodName)) {
108: methodName = MethodCollector.removePackagePrefix(className)
109: + "[static initializer]";
110: }
111: methodName = methodName.replaceAll("<", "_").replaceAll(">",
112: "_");
113:
114: // create a MethodInfo object to hold type signature
115: MethodInfo methodInfo = new MethodInfo(className, methodName,
116: reconstructedList);
117:
118: // if a new method was invoked, write out methodSet
119: if (methodSet.add(methodInfo)) {
120: String jblanketDir = getJBlanketDir();
121: String outFileName = jblanketDir + SLASH + COVER_PREFIX
122: + currentClassName + XML_EXTENSION;
123:
124: // get the current time as the last time outFileName was updated.
125: storeMethodData(methodSet, outFileName, currentClassName,
126: new Date());
127: }
128: }
129:
130: /**
131: * Verifies if <code>className</code> is a part of grammar defined by <code>testGrammar</code>.
132: * The only grammars acceptable are class names that either begins or ends with 'Test' and end
133: * with either '.java' or '.class' file types.
134: *
135: * @param className name of class to verify.
136: * @param testGrammar the grammar defining test class names.
137: * @return true if a part of <code>testGrammar</code>, false otherwise.
138: */
139: public static boolean isTestClass(String className,
140: String testGrammar) {
141:
142: // remove any '.class' or '.java' from testGrammar or last '.'
143: if (testGrammar.endsWith(".java")
144: || testGrammar.endsWith(".class")
145: || testGrammar.lastIndexOf('.') == testGrammar.length() - 1) {
146: testGrammar = testGrammar.substring(0, testGrammar
147: .lastIndexOf('.'));
148: }
149:
150: if (className.indexOf('.') > -1) {
151: // keep only the name of the class, removing package prefix
152: className = className.substring(
153: className.lastIndexOf('.') + 1, className.length());
154: }
155:
156: // check if className is part of testGrammar
157: if (testGrammar.endsWith("*")) {
158: String test = testGrammar.substring(0,
159: testGrammar.length() - 1);
160: return className.startsWith(test);
161: } else if (testGrammar.startsWith("*")) {
162: String test = testGrammar
163: .substring(1, testGrammar.length());
164: return className.endsWith(test);
165: }
166:
167: return false;
168: }
169:
170: /**
171: * Gets the output directory for intermediate and final JBlanket results. If the 'jblanket.dir'
172: * system property is not found, the default value is:
173: * <p>
174: * System.getProperty("user.home")\jblanket
175: *
176: * @return the JBlanket output directory.
177: */
178: public static String getJBlanketDir() {
179:
180: // 1. check the system properties
181: String jblanketDir = System.getProperty("jblanket.dir");
182:
183: // 2. check if in Tomcat and its conf/*.properties files
184: if (jblanketDir == null || jblanketDir.equals("")) {
185: jblanketDir = getJBlanketDirInTomcatProperties();
186: }
187:
188: // 3. jblanket.dir not found anywhere, so set default
189: if (jblanketDir == null || jblanketDir.equals("")) {
190: jblanketDir = System.getProperty("user.home") + SLASH
191: + "jblanket";
192: // keeping message as warning for those who might not have set directory with JUnit fork on
193: String message = "Warning: jblanket.dir system property missing. Using: "
194: + jblanketDir;
195: System.out.println(message);
196: // set default value
197: System.setProperty("jblanket.dir", jblanketDir);
198: }
199:
200: // create jblanket directory if does not exist
201: File jblanketDirectory = new File(jblanketDir);
202: if (!jblanketDirectory.exists()) {
203: jblanketDirectory.mkdirs();
204: }
205: return jblanketDir;
206: }
207:
208: /**
209: * Gets the 'jblanket.dir' property from Tomcat if the current JVM is running Tomcat.
210: * All *.properties files in the <CATALINA_HOME>/conf directory are loaded and
211: * checked. The first occurrence of 'jblanket.dir' is returned.
212: *
213: * @return the jblanket.dir property, or null if it doesn't exist.
214: */
215: private static String getJBlanketDirInTomcatProperties() {
216:
217: // First, try to get the Properties file using catalina.home.
218: File catalinaHome = new File(System.getProperty(
219: "catalina.home", ""));
220: if (!catalinaHome.exists()) {
221: return null;
222: }
223:
224: // look for the Tomcat/conf/.properties file(s)
225: File confDir = new File(catalinaHome, "conf");
226: DirectoryScanner scanner = new DirectoryScanner();
227: scanner.setBasedir(confDir);
228: scanner.setIncludes(new String[] { "*.properties" });
229: scanner.scan();
230: String[] files = scanner.getIncludedFiles();
231:
232: // check each file for jblanket.dir property
233: for (int i = 0; i < files.length; i++) {
234: Properties properties = new Properties();
235: try {
236: properties.load(new FileInputStream(new File(confDir,
237: files[i])));
238: } catch (Exception e) {
239: // do nothing -- if something is wrong with .properties file, don't care;
240: // instead assume that jblanket.dir property does not exist
241: }
242:
243: // return first occurrence of jblanket.dir
244: String tomcatJBlanketDir = properties.getProperty(
245: "jblanket.dir", null);
246: if (tomcatJBlanketDir != null) {
247: return tomcatJBlanketDir;
248: }
249: }
250:
251: // did not find jblanket.dir property
252: return null;
253: }
254:
255: /**
256: * Reconstructs the type signature of <code>type</code> from the way it is stored in the
257: * Constant Pool to the way it is found in the source code. This method is required because
258: * type signatures in the Constant Pool are different from the way it is coded.
259: * <p>
260: * For example: <pre>
261: * [Ljava/lang/String; ==> java.lang.String[]
262: * </pre>
263: *
264: * @param type the raw parameter type.
265: * @return the reconstructed parameter type.
266: */
267: public static String reconstructType(String type) {
268:
269: // if type is an array, find out how many dimensions
270: int arrayDimensions = 0;
271:
272: while (type.indexOf("[") > -1 && type.length() > 1) {
273: arrayDimensions++;
274: type = type.substring(1, type.length());
275: }
276:
277: StringBuffer reconstructedType = null;
278:
279: // base type
280: if (type.length() == 1) {
281: reconstructedType = new StringBuffer(
282: reconstructBaseType(type));
283: }
284: // object type
285: else {
286: // problem fixed: although 'L' denotes an object, this code fails when the name of a class
287: // starts with 'L'. Therefore, get the first instance of 'L' instead of last.
288: //int classNameStart = type.lastIndexOf('L') + 1;
289: int classNameStart = type.indexOf('L') + 1;
290: int classNameEnd = type.lastIndexOf(';');
291: type = type.substring(classNameStart, classNameEnd);
292: reconstructedType = new StringBuffer(type.replace('/', '.'));
293: }
294:
295: while (arrayDimensions > 0) {
296: reconstructedType.append("[]");
297: arrayDimensions--;
298: }
299: return reconstructedType.toString();
300: }
301:
302: /**
303: * Reconstructs a base type from the way it is stored in the Constant
304: * Pool to the way it is found in source code.
305: * <p>
306: * For example: <pre>
307: * I ==> int
308: * </pre>
309: *
310: * @param type the raw base type.
311: * @return the reconstructed base type.
312: */
313: private static String reconstructBaseType(String type) {
314:
315: char t = type.charAt(0);
316: switch (t) {
317: case 'B':
318: return "byte";
319: case 'C':
320: return "char";
321: case 'D':
322: return "double";
323: case 'F':
324: return "float";
325: case 'I':
326: return "int";
327: case 'J':
328: return "long";
329: case 'S':
330: return "short";
331: case 'Z':
332: return "boolean";
333: default:
334: // would throw an Exception, but difficult to implement since this is a piece of the
335: // method modification InstructionList
336: return "Error! type is an invalid string";
337: }
338: }
339:
340: /**
341: * Stores all the method information gathered in <code>jblanketSet</code> for
342: * <code>className</code> into <code>outFileName</code>.
343: *
344: * @param methodSet the collection of method information to output.
345: * @param outFileName the fully qualified path of the output file.
346: * @param className the name of the class.
347: * @param timeStamp the time stamp in milliseconds to include in file className.
348: * @throws JBlanketException if cannot find <code>outFileName</code> or
349: * if cannot write to <code>outFileName</code>.
350: */
351: public static synchronized void storeMethodData(
352: MethodSet methodSet, String outFileName, String className,
353: Date timeStamp) throws JBlanketException {
354:
355: // store test class results
356: MethodSet fileMethodSet = new MethodSet();
357: try {
358: FileInputStream istream = new FileInputStream(new File(
359: outFileName));
360: // throws ParseException, FileNotFoundException, IOException
361: fileMethodSet.load(istream);
362: istream.close();
363: } catch (Exception e) {
364: // do nothing. if file doesn't exist, don't care.
365: }
366:
367: FileOutputStream fostream = null;
368: try {
369: fostream = new FileOutputStream(outFileName);
370: } catch (FileNotFoundException e) {
371: throw new JBlanketException("Unable to open file "
372: + outFileName, e);
373: }
374:
375: fileMethodSet.union(methodSet);
376: try {
377: fileMethodSet.store(fostream, className, timeStamp);
378: } catch (IOException e) {
379: throw new JBlanketException("Unable to store MethodSet to "
380: + outFileName, e);
381: }
382:
383: try {
384: fostream.close();
385: } catch (IOException e) {
386: throw new JBlanketException("Unable to close file "
387: + outFileName, e);
388: }
389: }
390:
391: /**
392: * Removes the package prefix from <code>className</code>.
393: *
394: * @param className the fully qualified name of the class.
395: * @return the name of the class in <code>className</code>.
396: */
397: public static String removePackagePrefix(String className) {
398: return className.substring(className.lastIndexOf('.') + 1,
399: className.length());
400: }
401:
402: /**
403: * Gets the Date format used by JBlanket.
404: *
405: * @return the DateFormat used by JBlanket.
406: */
407: public static DateFormat getDateFormat() {
408: return new SimpleDateFormat("MMM d, yyyy h:mm:ss a");
409: }
410: }
|