Prepends the proper amount of indentation on code snippets written in to the benchmark reports.

Pair programmed.

Changes by: bruce, toby


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@952 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/util/Util.java b/dev/core/src/com/google/gwt/dev/util/Util.java
index 99ba8ed..11a52cb 100644
--- a/dev/core/src/com/google/gwt/dev/util/Util.java
+++ b/dev/core/src/com/google/gwt/dev/util/Util.java
@@ -956,6 +956,18 @@
     return true;
   }
 
+  /**
+   * Escapes '&', '<', '>', '"', and '\'' to their XML entity equivalents.
+   */
+  private static String escapeXml(String unescaped) {
+    String escaped = unescaped.replaceAll("\\&", "&amp;");
+    escaped = escaped.replaceAll("\\<", "&lt;");
+    escaped = escaped.replaceAll("\\>", "&gt;");
+    escaped = escaped.replaceAll("\\\"", "&quot;");
+    escaped = escaped.replaceAll("\\'", "&apos;");
+    return escaped;
+  }
+
   private static void writeAttribute(PrintWriter w, Attr attr, int depth)
       throws IOException {
     w.write(attr.getName());
@@ -1053,7 +1065,9 @@
   }
 
   private static void writeText(PrintWriter w, Text text) throws DOMException {
-    w.write(text.getNodeValue());
+    String nodeValue = text.getNodeValue();
+    String escaped = escapeXml(nodeValue);
+    w.write(escaped);
   }
 
   /**
diff --git a/user/src/com/google/gwt/junit/benchmarks/BenchmarkReport.java b/user/src/com/google/gwt/junit/benchmarks/BenchmarkReport.java
index ffbe2b9..8e8fd7f 100644
--- a/user/src/com/google/gwt/junit/benchmarks/BenchmarkReport.java
+++ b/user/src/com/google/gwt/junit/benchmarks/BenchmarkReport.java
@@ -1,12 +1,12 @@
 /*
  * Copyright 2007 Google Inc.
- *
+ * 
  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  * use this file except in compliance with the License. You may obtain a copy of
  * the License at
- *
+ * 
  * http://www.apache.org/licenses/LICENSE-2.0
- *
+ * 
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -15,62 +15,57 @@
  */
 package com.google.gwt.junit.benchmarks;
 
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.core.ext.typeinfo.HasMetaData;
 import com.google.gwt.core.ext.typeinfo.JClassType;
 import com.google.gwt.core.ext.typeinfo.JMethod;
 import com.google.gwt.core.ext.typeinfo.TypeOracle;
-import com.google.gwt.core.ext.TreeLogger;
-import com.google.gwt.junit.rebind.BenchmarkGenerator;
+import com.google.gwt.dev.util.Util;
 import com.google.gwt.junit.client.TestResults;
 import com.google.gwt.junit.client.Trial;
+import com.google.gwt.junit.rebind.BenchmarkGenerator;
+import com.google.gwt.util.tools.Utility;
 
 import junit.framework.TestCase;
 
+import org.eclipse.jdt.internal.compiler.IProblemFactory;
+import org.eclipse.jdt.internal.compiler.ISourceElementRequestor;
+import org.eclipse.jdt.internal.compiler.SourceElementParser;
+import org.eclipse.jdt.internal.compiler.SourceElementRequestorAdapter;
+import org.eclipse.jdt.internal.compiler.batch.CompilationUnit;
+import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
+import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
-import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
-import org.eclipse.jdt.internal.compiler.IProblemFactory;
-import org.eclipse.jdt.internal.compiler.SourceElementParser;
-import org.eclipse.jdt.internal.compiler.ISourceElementRequestor;
-import org.eclipse.jdt.internal.compiler.SourceElementRequestorAdapter;
-import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory;
-import org.eclipse.jdt.internal.compiler.batch.CompilationUnit;
 
-import java.io.IOException;
-import java.io.FileOutputStream;
-import java.io.BufferedOutputStream;
-import java.io.File;
-import java.io.FileReader;
 import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.text.BreakIterator;
+import java.text.DateFormat;
+import java.util.ArrayList;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Iterator;
-import java.util.Map;
 import java.util.List;
-import java.util.ArrayList;
 import java.util.Locale;
-import java.util.regex.Pattern;
+import java.util.Map;
 import java.util.regex.Matcher;
-import java.text.DateFormat;
-import java.text.BreakIterator;
+import java.util.regex.Pattern;
 
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.transform.TransformerFactory;
-import javax.xml.transform.Transformer;
-import javax.xml.transform.OutputKeys;
-import javax.xml.transform.TransformerException;
-import javax.xml.transform.stream.StreamResult;
-import javax.xml.transform.dom.DOMSource;
 
 /**
  * Generates a detailed report that contains the results of all of the
  * benchmark-related unit tests executed during a unit test session. The primary
  * user of this class is JUnitShell.
- *
+ * 
  * The report is in XML format. To view the XML reports, use benchmarkViewer.
- *
  */
 public class BenchmarkReport {
 
@@ -81,17 +76,15 @@
 
     private MetaData metaData;
 
-    private List/*<JUnitMessageQueue.TestResult>*/ results;
+    private List/* <JUnitMessageQueue.TestResult> */results;
 
     private TestCase test;
 
     BenchmarkXml(TestCase test,
-        List/*<JUnitMessageQueue.TestResult>*/ results) {
+        List/* <JUnitMessageQueue.TestResult> */results) {
       this.test = test;
       this.results = results;
-      Map/*<String,MetaData>*/ methodMetaData
-          = (Map/*<String,MetaData>*/) testMetaData
-          .get(test.getClass().toString());
+      Map/* <String,MetaData> */methodMetaData = (Map/* <String,MetaData> */) testMetaData.get(test.getClass().toString());
       metaData = (MetaData) methodMetaData.get(test.getName());
     }
 
@@ -149,8 +142,8 @@
         trialElement.appendChild(variableElement);
       }
 
-      trialElement
-          .setAttribute("timing", String.valueOf(trial.getRunTimeMillis()));
+      trialElement.setAttribute("timing",
+          String.valueOf(trial.getRunTimeMillis()));
 
       Throwable exception = trial.getException();
 
@@ -166,11 +159,11 @@
 
   /**
    * Parses a .java source file to get the source code for methods.
-   *
+   * 
    * This Parser takes some shortcuts based on the fact that it's only being
    * used to locate test methods for unit tests. (For example, only requiring a
    * method name instead of a full type signature for lookup).
-   *
+   * 
    * TODO(tobyr) I think that I might be able to replace all this code with a
    * call to the existing metadata interface. Check declEnd/declStart in
    * JAbstractMethod.
@@ -179,7 +172,7 @@
 
     static class MethodBody {
 
-      int declarationEnd;   // the character index of the end of the method
+      int declarationEnd; // the character index of the end of the method
 
       int declarationStart; // the character index of the start of the method
 
@@ -189,8 +182,7 @@
     private MethodBody currentMethod; // Only used during the visitor
 
     // But it's less painful
-    private Map/*<String,MethodBody>*/ methods
-        = new HashMap/*<String,MethodBody>*/();
+    private Map/* <String,MethodBody> */methods = new HashMap/* <String,MethodBody> */();
 
     // Contains the contents of the entire source file
     private char[] sourceContents;
@@ -245,22 +237,17 @@
 
     /**
      * Returns the source code for the method of the given name.
-     *
+     * 
      * @return null if the source code for the method can not be located
      */
     public String getMethod(JMethod method) {
       /*
-      MethodBody methodBody = (MethodBody)methods.get( method.getName() );
-      if ( methodBody == null ) {
-        return null;
-      }
-      if ( methodBody.source == null ) {
-        methodBody.source = new String(sourceContents,
-        methodBody.declarationStart, methodBody.declarationEnd - methodBody.
-        declarationStart + 1);
-      }
-      return methodBody.source;
-      */
+       * MethodBody methodBody = (MethodBody)methods.get( method.getName() ); if (
+       * methodBody == null ) { return null; } if ( methodBody.source == null ) {
+       * methodBody.source = new String(sourceContents,
+       * methodBody.declarationStart, methodBody.declarationEnd - methodBody.
+       * declarationStart + 1); } return methodBody.source;
+       */
       return new String(sourceContents, method.getDeclStart(),
           method.getDeclEnd() - method.getDeclStart() + 1);
     }
@@ -271,21 +258,40 @@
    */
   private class ReportXml {
 
-    private Map/*<String,Element>*/ categoryElementMap
-        = new HashMap/*<String,Element>*/();
+    private Map/* <String,Element> */categoryElementMap = new HashMap/* <String,Element> */();
 
     private Date date = new Date();
 
     private String version = "unknown";
 
+    Element toElement(Document doc) {
+      Element report = doc.createElement("gwt_benchmark_report");
+      String dateString = DateFormat.getDateTimeInstance().format(date);
+      report.setAttribute("date", dateString);
+      report.setAttribute("gwt_version", version);
+
+      // - Add each test result into the report.
+      // - Add the category for the test result, if necessary.
+      for (Iterator entryIt = testResults.entrySet().iterator(); entryIt.hasNext();) {
+        Map.Entry entry = (Map.Entry) entryIt.next();
+        TestCase test = (TestCase) entry.getKey();
+        List/* <JUnitMessageQueue.TestResult> */results = (List/* <JUnitMessageQueue.TestResult> */) entry.getValue();
+        BenchmarkXml xml = new BenchmarkXml(test, results);
+        Element categoryElement = getCategoryElement(doc, report,
+            xml.metaData.getCategory().getClassName());
+        categoryElement.appendChild(xml.toElement(doc));
+      }
+
+      return report;
+    }
+
     /**
      * Locates or creates the category element by the specified name.
-     *
+     * 
      * @param doc The document to search
      * @return The matching category element
      */
-    private Element getCategoryElement(Document doc, Element report,
-        String name) {
+    private Element getCategoryElement(Document doc, Element report, String name) {
       Element e = (Element) categoryElementMap.get(name);
 
       if (e != null) {
@@ -302,42 +308,18 @@
 
       return categoryElement;
     }
-
-    Element toElement(Document doc) {
-      Element report = doc.createElement("gwt_benchmark_report");
-      String dateString = DateFormat.getDateTimeInstance().format(date);
-      report.setAttribute("date", dateString);
-      report.setAttribute("gwt_version", version);
-
-      // - Add each test result into the report.
-      // - Add the category for the test result, if necessary.
-      for (Iterator entryIt = testResults.entrySet().iterator();
-          entryIt.hasNext();) {
-        Map.Entry entry = (Map.Entry) entryIt.next();
-        TestCase test = (TestCase) entry.getKey();
-        List/*<JUnitMessageQueue.TestResult>*/ results
-            = (List/*<JUnitMessageQueue.TestResult>*/) entry.getValue();
-        BenchmarkXml xml = new BenchmarkXml(test, results);
-        Element categoryElement = getCategoryElement(doc, report,
-            xml.metaData.getCategory().getClassName());
-        categoryElement.appendChild(xml.toElement(doc));
-      }
-
-      return report;
-    }
   }
 
   private static final String GWT_BENCHMARK_CATEGORY = "gwt.benchmark.category";
 
-  private static final String GWT_BENCHMARK_DESCRIPTION
-      = "gwt.benchmark.description";
+  private static final String GWT_BENCHMARK_DESCRIPTION = "gwt.benchmark.description";
 
   private static final String GWT_BENCHMARK_NAME = "gwt.benchmark.name";
 
   private static File findSourceFile(JClassType klass) {
     final char separator = File.separator.charAt(0);
-    String filePath = klass.getPackage().getName().replace('.', separator) +
-        separator + klass.getSimpleSourceName() + ".java";
+    String filePath = klass.getPackage().getName().replace('.', separator)
+        + separator + klass.getSimpleSourceName() + ".java";
     String[] paths = getClassPath();
 
     for (int i = 0; i < paths.length; ++i) {
@@ -356,8 +338,7 @@
     return path.split(File.pathSeparator);
   }
 
-  private static String getSimpleMetaData(HasMetaData hasMetaData,
-      String name) {
+  private static String getSimpleMetaData(HasMetaData hasMetaData, String name) {
     String[][] allValues = hasMetaData.getMetaData(name);
 
     if (allValues == null) {
@@ -394,26 +375,23 @@
       source.append("\n");
     }
 
-    char[] buf = new char[ source.length() ];
+    char[] buf = new char[source.length()];
     source.getChars(0, buf.length, buf, 0);
 
     return buf;
   }
 
-  private Map/*<String,Map<CategoryImpl>*/ testCategories
-      = new HashMap/*<String,CategoryImpl>*/();
+  private Map/* <String,Map<CategoryImpl> */testCategories = new HashMap/* <String,CategoryImpl> */();
 
-  private Map/*<String,Map<String,MetaData>>*/ testMetaData
-      = new HashMap/*<String,Map<String,MetaData>>*/();
+  private Map/* <String,Map<String,MetaData>> */testMetaData = new HashMap/* <String,Map<String,MetaData>> */();
 
-  private Map/*<TestCase,List<JUnitMessageQueue.TestResult>>*/ testResults
-      = new HashMap/*<TestCase,JUnitMessageQueue.List<TestResult>>*/();
+  private Map/* <TestCase,List<JUnitMessageQueue.TestResult>> */testResults = new HashMap/* <TestCase,JUnitMessageQueue.List<TestResult>> */();
 
   private TypeOracle typeOracle;
 
   private TreeLogger logger;
 
-  public BenchmarkReport( TreeLogger logger ) {
+  public BenchmarkReport(TreeLogger logger) {
     this.logger = logger;
   }
 
@@ -427,20 +405,17 @@
     String categoryType = getSimpleMetaData(benchmarkClass,
         GWT_BENCHMARK_CATEGORY);
 
-    Map zeroArgMethods = BenchmarkGenerator
-        .getNotOverloadedTestMethods(benchmarkClass);
-    Map/*<String,JMethod>*/ parameterizedMethods = BenchmarkGenerator
-        .getParameterizedTestMethods(benchmarkClass, TreeLogger.NULL);
-    List/*<JMethod>*/ testMethods = new ArrayList/*<JMethod>*/(
+    Map zeroArgMethods = BenchmarkGenerator.getNotOverloadedTestMethods(benchmarkClass);
+    Map/* <String,JMethod> */parameterizedMethods = BenchmarkGenerator.getParameterizedTestMethods(
+        benchmarkClass, TreeLogger.NULL);
+    List/* <JMethod> */testMethods = new ArrayList/* <JMethod> */(
         zeroArgMethods.size() + parameterizedMethods.size());
     testMethods.addAll(zeroArgMethods.values());
     testMethods.addAll(parameterizedMethods.values());
 
-    Map/*<String,MetaData>*/ metaDataMap
-        = (Map/*<String,MetaData>*/) testMetaData
-        .get(benchmarkClass.toString());
+    Map/* <String,MetaData> */metaDataMap = (Map/* <String,MetaData> */) testMetaData.get(benchmarkClass.toString());
     if (metaDataMap == null) {
-      metaDataMap = new HashMap/*<String,MetaData>*/();
+      metaDataMap = new HashMap/* <String,MetaData> */();
       testMetaData.put(benchmarkClass.toString(), metaDataMap);
     }
 
@@ -451,8 +426,8 @@
     } catch (IOException e) {
       // if we fail to create the parser for some reason, we'll have to just
       // deal with a null parser.
-      logger.log(TreeLogger.WARN,
-          "Unable to parse the code for " + benchmarkClass, e);
+      logger.log(TreeLogger.WARN, "Unable to parse the code for "
+          + benchmarkClass, e);
     }
 
     // Add all of the benchmark methods
@@ -465,22 +440,23 @@
         methodCategoryType = categoryType;
       }
       CategoryImpl methodCategory = getCategory(methodCategoryType);
-      String sourceCode = parser == null ? null : parser.getMethod(method);
+      StringBuffer sourceCode = parser == null ? null : new StringBuffer(
+          parser.getMethod(method));
       StringBuffer summary = new StringBuffer();
       StringBuffer comment = new StringBuffer();
       getComment(sourceCode, summary, comment);
 
       MetaData metaData = new MetaData(benchmarkClass.toString(), methodName,
-          sourceCode, methodCategory, methodName, summary.toString());
+          sourceCode != null ? sourceCode.toString() : null, methodCategory,
+          methodName, summary.toString());
       metaDataMap.put(methodName, metaData);
     }
   }
 
   public void addBenchmarkResults(TestCase test, TestResults results) {
-    List/*<TestResults>*/ currentResults = (List/*<TestResults>*/) testResults
-        .get(test);
+    List/* <TestResults> */currentResults = (List/* <TestResults> */) testResults.get(test);
     if (currentResults == null) {
-      currentResults = new ArrayList/*<TestResults>*/();
+      currentResults = new ArrayList/* <TestResults> */();
       testResults.put(test, currentResults);
     }
     currentResults.add(results);
@@ -489,12 +465,15 @@
   /**
    * Generates reports for all of the benchmarks which were added to the
    * generator.
-   *
+   * 
    * @param outputPath The path to write the reports to.
+   * @throws ParserConfigurationException
+   * @throws UnableToCompleteException
+   * @throws IOException
    * @throws IOException If anything goes wrong writing to outputPath
    */
-  public void generate(String outputPath)
-      throws IOException, TransformerException, ParserConfigurationException {
+  public void generate(String outputPath) throws ParserConfigurationException,
+      UnableToCompleteException, IOException {
 
     // Don't generate a new report if no tests were actually run.
     if (testResults.size() == 0) {
@@ -503,25 +482,34 @@
 
     DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
     DocumentBuilder builder = factory.newDocumentBuilder();
-
     Document doc = builder.newDocument();
     doc.appendChild(new ReportXml().toElement(doc));
+    byte[] xmlBytes = Util.toXmlUtf8(doc);
+    FileOutputStream fos = null;
+    try {
+      fos = new FileOutputStream(outputPath);
+      fos.write(xmlBytes);
+    } finally {
+      Utility.close(fos);
+    }
 
-    // TODO(tobyr) Looks like indenting is busted
-    // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6296446
-    // Not a big deal, since we don't intend to read the XML by hand anyway
-    TransformerFactory transformerFactory = TransformerFactory.newInstance();
-    // Think this can be used with JDK 1.5
-    // transformerFactory.setAttribute( "indent-number", new Integer(2) );
-    Transformer serializer = transformerFactory.newTransformer();
-    serializer.setOutputProperty(OutputKeys.METHOD, "xml");
-    serializer.setOutputProperty(OutputKeys.INDENT, "yes");
-    serializer
-        .setOutputProperty("{ http://xml.apache.org/xslt }indent-amount", "2");
-    BufferedOutputStream docOut = new BufferedOutputStream(
-        new FileOutputStream(outputPath));
-    serializer.transform(new DOMSource(doc), new StreamResult(docOut));
-    docOut.close();
+    // TODO(bruce) The code below is commented out because of GWT Issue 958.
+
+    // // TODO(tobyr) Looks like indenting is busted
+    // // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6296446
+    // // Not a big deal, since we don't intend to read the XML by hand anyway
+    // TransformerFactory transformerFactory = TransformerFactory.newInstance();
+    // // Think this can be used with JDK 1.5
+    // // transformerFactory.setAttribute( "indent-number", new Integer(2) );
+    // Transformer serializer = transformerFactory.newTransformer();
+    // serializer.setOutputProperty(OutputKeys.METHOD, "xml");
+    // serializer.setOutputProperty(OutputKeys.INDENT, "yes");
+    // serializer
+    // .setOutputProperty("{ http://xml.apache.org/xslt }indent-amount", "2");
+    // BufferedOutputStream docOut = new BufferedOutputStream(
+    // new FileOutputStream(outputPath));
+    // serializer.transform(new DOMSource(doc), new StreamResult(docOut));
+    // docOut.close();
   }
 
   private CategoryImpl getCategory(String name) {
@@ -554,7 +542,7 @@
    * first sentence summary in <code>summary</code> and the body of the entire
    * comment (including the summary) in <code>comment</code>.
    */
-  private void getComment(String sourceCode, StringBuffer summary,
+  private void getComment(StringBuffer sourceCode, StringBuffer summary,
       StringBuffer comment) {
 
     if (sourceCode == null) {
@@ -569,20 +557,20 @@
     Pattern p = Pattern.compile(regex, Pattern.DOTALL);
     Matcher m = p.matcher(sourceCode);
 
-    if (! m.find()) {
+    // Early out if there is no javadoc comment.
+    if (!m.find()) {
       return;
     }
 
     String commentStr = m.group();
 
-    p = Pattern.compile(
-        "(/\\*\\*\\s*)" +  // The comment header
+    p = Pattern.compile("(/\\*\\*\\s*)" + // The comment header
         "(((\\s*\\**\\s*)([^\n\r]*)[\n\r]+)*)" // The comment body
     );
 
     m = p.matcher(commentStr);
 
-    if (! m.find()) {
+    if (!m.find()) {
       return;
     }
 
@@ -601,5 +589,25 @@
     }
 
     comment.append(bareComment);
+
+    // Measure the indentation width on the second line to infer what the
+    // first line indent should be.
+    p = Pattern.compile("[^\\r\\n]+[\\r\\n]+(\\s+)\\*", Pattern.MULTILINE);
+    m = p.matcher(sourceCode);
+    int indentLen = 0;
+    if (m.find()) {
+      String indent = m.group(1);
+      indentLen = indent.length() - 1;
+    }
+    StringBuffer leadingIndent = new StringBuffer();
+    for (int i = 0; i < indentLen; ++i) {
+      leadingIndent.append(' ');
+    }
+
+    // By inserting at 0 here, we are assuming that sourceCode begins with
+    // /**, which is actually a function of how JDT sees a declaration start.
+    // If in the future, you see bogus indentation here, it means that this
+    // assumption is bad.
+    sourceCode.insert(0, leadingIndent);
   }
-}
\ No newline at end of file
+}