Makes part of the Compiler Report (SOYC) smaller by
replacing a flat HTML output with output
that is generated with JavaScript from a dictionary of
strings.  This decreases the size of the dependency
reports by a factor of 5 and the overall report by a
factor of 4.  There is a difference in the time
to display for a report of a large app when running in
Chrome for what was a 5MB report. (It takes longer to
build the report using javascript)

Review at http://gwt-code-reviews.appspot.com/1123801

Review by: kprobst@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9265 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/soyc/MakeTopLevelHtmlForPerm.java b/dev/core/src/com/google/gwt/soyc/MakeTopLevelHtmlForPerm.java
index 51a7949..509f3ec 100644
--- a/dev/core/src/com/google/gwt/soyc/MakeTopLevelHtmlForPerm.java
+++ b/dev/core/src/com/google/gwt/soyc/MakeTopLevelHtmlForPerm.java
@@ -34,6 +34,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -154,6 +155,52 @@
     outFile.close();
   }
 
+  /**
+   * @return given "com.foo.myClass" or "com.foo.myClass::myMethod" returns
+   *         "myClass"
+   */
+  static String getClassSubstring(String fullMethodName) {
+    if (fullMethodName.length() == 0) {
+      return "";
+    }
+    int startIndex = getPackageSubstring(fullMethodName).length() + 1;
+    int endIndex = fullMethodName.indexOf("::");
+    if (endIndex == -1) {
+      endIndex = fullMethodName.length();
+    }
+    if (startIndex > endIndex || startIndex > fullMethodName.length()) {
+      return "";
+    }
+    return fullMethodName.substring(startIndex, endIndex);
+  }
+
+  /**
+   * @return given "com.foo.myClass::myMethod" returns "myMethod"
+   */
+  static String getMethodSubstring(String fullMethodName) {
+    int index = fullMethodName.indexOf("::");
+    if (index == -1) {
+      return "";
+    }
+    index += 2;
+    if (index >= fullMethodName.length()) {
+      return "";
+    }
+    return fullMethodName.substring(index);
+  }
+
+  /**
+   * @return given "com.foo.myClass" or "com.foo.myClass::myMethod" returns
+   *         "com.foo"
+   */
+  static String getPackageSubstring(String fullMethodName) {
+    int endIndex = fullMethodName.lastIndexOf('.');
+    if (endIndex == -1) {
+      endIndex = fullMethodName.length();
+    }
+    return fullMethodName.substring(0, endIndex);
+  }
+
   private static void addSmallHtmlProlog(final PrintWriter outFile, String title) {
     outFile.println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\"");
     outFile.println("\"http://www.w3.org/TR/html4/strict.dtd\">");
@@ -287,11 +334,11 @@
 
     String popupName = "packageBreakdownPopup";
     String popupTitle = "Package breakdown";
-    String popupBody =  "The package breakdown blames pieces of JavaScript "
-      + "code on Java packages wherever possible.  Note that this is not possible for all code, so the sizes "
-      + "of the packages here will not normally add up to the full code size.  More specifically, the sum will "
-      + "exclude strings, whitespace, and a few pieces of JavaScript code that are produced during compilation "
-      + "but cannot be attributed to any Java package.";
+    String popupBody = "The package breakdown blames pieces of JavaScript "
+        + "code on Java packages wherever possible.  Note that this is not possible for all code, so the sizes "
+        + "of the packages here will not normally add up to the full code size.  More specifically, the sum will "
+        + "exclude strings, whitespace, and a few pieces of JavaScript code that are produced during compilation "
+        + "but cannot be attributed to any Java package.";
 
     outFile.println("<h2>");
     addPopupLink(outFile, popupName, popupTitle, null);
@@ -303,9 +350,9 @@
     popupName = "codeTypeBreakdownPopup";
     popupTitle = "Code Type Breakdown";
     popupBody = "The code type breakdown breaks down the JavaScript code according to its "
-      + "type or function.  For example, it tells you how much of your code can be attributed to "
-      + "JRE, GWT-RPC, etc.  As above, strings and some other JavaScript snippets are not included "
-      + "in the breakdown.";
+        + "type or function.  For example, it tells you how much of your code can be attributed to "
+        + "JRE, GWT-RPC, etc.  As above, strings and some other JavaScript snippets are not included "
+        + "in the breakdown.";
     outFile.println("<h2>");
     addPopupLink(outFile, popupName, popupTitle, null);
     outFile.println("</h2>");
@@ -390,10 +437,9 @@
     }
   }
 
-  public void makeCompilerMetricsPermFiles (
-      ModuleMetricsArtifact moduleMetrics,
+  public void makeCompilerMetricsPermFiles(ModuleMetricsArtifact moduleMetrics,
       PrecompilationMetricsArtifact precompilationMetrics,
-      CompilationMetricsArtifact compilationMetrics)  throws IOException {
+      CompilationMetricsArtifact compilationMetrics) throws IOException {
     String outFileName = "CompilerMetrics-"
         + precompilationMetrics.getPermuationBase() + "-index.html";
     PrintWriter outFile = new PrintWriter(getOutFile(outFileName));
@@ -427,7 +473,8 @@
     outFile.println("<td>");
     outFile.println("Precompile (may include Module Analysis)");
     outFile.println("</td>");
-    outFile.println("<td>" + precompilationMetrics.getElapsedMilliseconds() + " ms");
+    outFile.println("<td>" + precompilationMetrics.getElapsedMilliseconds()
+        + " ms");
     outFile.println("</td>");
     outFile.println("</tr>");
 
@@ -435,7 +482,8 @@
     outFile.println("<td>");
     outFile.println("Compile");
     outFile.println("</td>");
-    outFile.println("<td>" + compilationMetrics.getElapsedMilliseconds() + " ms");
+    outFile.println("<td>" + compilationMetrics.getElapsedMilliseconds()
+        + " ms");
     outFile.println("</td>");
     outFile.println("</tr>");
     outFile.println("</table>");
@@ -468,7 +516,8 @@
     outFile.println("</tr>");
     makeCompilerMetricsSources(sourcesFileName, moduleMetrics, popupBody);
 
-    String initialTypesFileName = "CompilerMetrics-initialTypes-" + permutationId + ".html";
+    String initialTypesFileName = "CompilerMetrics-initialTypes-"
+        + permutationId + ".html";
     outFile.println("<tr>");
     outFile.println("<td>");
     popupName = "compilerMetricsInitialTypes";
@@ -481,9 +530,11 @@
     outFile.println("" + moduleMetrics.getInitialTypes().length);
     outFile.println("</td>");
     outFile.println("</tr>");
-    makeCompilerMetricsInitialTypeOracleTypes(initialTypesFileName, moduleMetrics, popupBody);
+    makeCompilerMetricsInitialTypeOracleTypes(initialTypesFileName,
+        moduleMetrics, popupBody);
 
-    String finalTypesFileName = "CompilerMetrics-finalTypes-" + permutationId + ".html";
+    String finalTypesFileName = "CompilerMetrics-finalTypes-" + permutationId
+        + ".html";
     outFile.println("<tr>");
     outFile.println("<td>");
     popupName = "compilerMetricsFinalTypes";
@@ -496,10 +547,13 @@
     outFile.println("" + precompilationMetrics.getFinalTypeOracleTypes().length);
     outFile.println("</td>");
     outFile.println("</tr>");
-    makeCompilerMetricsFinalTypeOracleTypes(finalTypesFileName, precompilationMetrics, popupBody);
+    makeCompilerMetricsFinalTypeOracleTypes(finalTypesFileName,
+        precompilationMetrics, popupBody);
 
-    String[] generatedTypes = getGeneratedTypes(moduleMetrics, precompilationMetrics);
-    String generatedTypesFileName = "CompilerMetrics-generatedTypes-" + permutationId + ".html";
+    String[] generatedTypes = getGeneratedTypes(moduleMetrics,
+        precompilationMetrics);
+    String generatedTypesFileName = "CompilerMetrics-generatedTypes-"
+        + permutationId + ".html";
     outFile.println("<tr>");
     outFile.println("<td>");
     popupName = "compilerMetricsGeneratedTypes";
@@ -512,7 +566,8 @@
     outFile.println("" + generatedTypes.length);
     outFile.println("</td>");
     outFile.println("</tr>");
-    makeCompilerMetricsGeneratedTypes(generatedTypesFileName, generatedTypes, popupBody);
+    makeCompilerMetricsGeneratedTypes(generatedTypesFileName, generatedTypes,
+        popupBody);
 
     String astFileName = "CompilerMetrics-ast-" + permutationId + ".html";
     outFile.println("<tr>");
@@ -531,7 +586,8 @@
     makeCompilerMetricsAstTypes(astFileName, precompilationMetrics, popupBody);
 
     String[] unreferencedTypes = getUnreferencedTypes(precompilationMetrics);
-    String unreferencedFileName = "CompilerMetrics-unreferencedTypes-" + permutationId + ".html";
+    String unreferencedFileName = "CompilerMetrics-unreferencedTypes-"
+        + permutationId + ".html";
     outFile.println("<tr>");
     outFile.println("<td>");
     popupName = "compilerMetricsUnreferenceTypes";
@@ -545,7 +601,8 @@
     outFile.println("" + unreferencedTypes.length);
     outFile.println("</td>");
     outFile.println("</tr>");
-    makeCompilerMetricsUnreferencedTypes(unreferencedFileName, unreferencedTypes, popupBody);
+    makeCompilerMetricsUnreferencedTypes(unreferencedFileName,
+        unreferencedTypes, popupBody);
     outFile.println("</table>");
 
     addStandardHtmlEnding(outFile);
@@ -797,7 +854,8 @@
     outFile.println("</ul>");
   }
 
-  private void addPopup(PrintWriter outFile, String popupName, String popupTitle, String popupBody) {
+  private void addPopup(PrintWriter outFile, String popupName,
+      String popupTitle, String popupBody) {
     outFile.println("<div class=\"soyc-popup\" id=\"" + popupName + "\">");
     outFile.println("<table>");
     outFile.println("<tr><th><b>" + popupTitle + "</b></th></tr>");
@@ -812,8 +870,9 @@
     if (href != null) {
       outFile.println("href=\"" + href + "\"");
     }
-    outFile.println("style=\"cursor:default;\" onMouseOver=\"show('" + popupName + "');\" "
-        + "onMouseOut=\"hide('" + popupName + "');\">" + popupTitle + "</a>");
+    outFile.println("style=\"cursor:default;\" onMouseOver=\"show('"
+        + popupName + "');\" " + "onMouseOut=\"hide('" + popupName + "');\">"
+        + popupTitle + "</a>");
   }
 
   /**
@@ -846,8 +905,8 @@
   }
 
   /**
-   * Return a {@link java.io.File} object for a file to be emitted into the output
-   * directory.
+   * Return a {@link java.io.File} object for a file to be emitted into the
+   * output directory.
    */
   private OutputStream getOutFile(String localFileName) throws IOException {
     return outDir.getOutputStream(localFileName);
@@ -860,7 +919,8 @@
     return globalInformation.getPermutationId();
   }
 
-  private String[] getUnreferencedTypes(PrecompilationMetricsArtifact precompilationMetrics) {
+  private String[] getUnreferencedTypes(
+      PrecompilationMetricsArtifact precompilationMetrics) {
     List<String> astTypes = Lists.create(precompilationMetrics.getAstTypes());
     Set<String> unreferencedTypes = Sets.create(precompilationMetrics.getFinalTypeOracleTypes());
     unreferencedTypes.removeAll(astTypes);
@@ -1005,7 +1065,8 @@
   }
 
   private void makeCompilerMetricsAstTypes(String outFileName,
-      PrecompilationMetricsArtifact precompilationMetrics, String helpText) throws IOException {
+      PrecompilationMetricsArtifact precompilationMetrics, String helpText)
+      throws IOException {
     PrintWriter outFile = new PrintWriter(getOutFile(outFileName));
     String title = "AST Types";
     addStandardHtmlProlog(outFile, title, title, "");
@@ -1024,7 +1085,8 @@
   }
 
   private void makeCompilerMetricsFinalTypeOracleTypes(String outFileName,
-      PrecompilationMetricsArtifact precompilationMetrics, String helpText) throws IOException {
+      PrecompilationMetricsArtifact precompilationMetrics, String helpText)
+      throws IOException {
     PrintWriter outFile = new PrintWriter(getOutFile(outFileName));
     String title = "Final Type Oracle Types";
     addStandardHtmlProlog(outFile, title, title, "");
@@ -1078,8 +1140,8 @@
     outFile.close();
   }
 
-  private void makeCompilerMetricsSources(String outFileName, ModuleMetricsArtifact moduleMetrics,
-      String helpText) throws IOException {
+  private void makeCompilerMetricsSources(String outFileName,
+      ModuleMetricsArtifact moduleMetrics, String helpText) throws IOException {
     PrintWriter outFile = new PrintWriter(getOutFile(outFileName));
     String title = "Sources on Source Path";
     addStandardHtmlProlog(outFile, title, title, "");
@@ -1123,65 +1185,148 @@
    */
   private void makeDependenciesHtml(String depGraphName,
       Map<String, String> dependencies) throws IOException {
-    String depGraphDescription = inferDepGraphDescription(depGraphName);
-    PrintWriter outFile = null;
     String curPackageName = "";
-    String curClassName = "";
 
+    // Separate out the packages to write them into different HTML files.
+    String packageName = "";
+    List<String> classesInPackage = new ArrayList<String>();
     for (String method : dependencies.keySet()) {
       // this key set is already in alphabetical order
       // get the package of this method, i.e., everything up to .[A-Z]
+      packageName = method.replaceAll("\\.\\p{Upper}.*", "");
 
-      String packageName = method;
-      packageName = packageName.replaceAll("\\.\\p{Upper}.*", "");
-
-      String className = method;
-      className = className.replaceAll("::.*", "");
-
-      if ((curPackageName.compareTo("") == 0)
-          || (curPackageName.compareTo(packageName) != 0)) {
-
+      if (curPackageName.compareTo("") == 0) {
         curPackageName = packageName;
-        if (outFile != null) {
-          // finish up the current file
-          addStandardHtmlEnding(outFile);
-          outFile.close();
+      } else if (curPackageName.compareTo(packageName) != 0) {
+        makeDependenciesInternedHtml(depGraphName, curPackageName,
+            classesInPackage, dependencies);
+        classesInPackage = new ArrayList<String>();
+        curPackageName = packageName;
+      }
+      classesInPackage.add(method);
+    }
+    if (classesInPackage.size() > 0) {
+      makeDependenciesInternedHtml(depGraphName, curPackageName,
+          classesInPackage, dependencies);
+    }
+  }
+
+  /**
+   * Produces an HTML file that displays dependencies.
+   *
+   * @param depGraphName name of dependency graph
+   * @param dependencies map of dependencies
+   * @throws IOException
+   */
+  private void makeDependenciesInternedHtml(String depGraphName,
+      String packageName, List<String> classesInPackage,
+      Map<String, String> dependencies) throws IOException {
+    String depGraphDescription = inferDepGraphDescription(depGraphName);
+    PrintWriter outFile = null;
+    String curClassName = "";
+
+    // To save space, create a JS array of all possible methods in this report.
+    class HtmlInterner extends LinkedHashMap<String, Integer> {
+      int index = 0;
+
+      public void intern(String key) {
+        if (!containsKey(key)) {
+          put(key, index++);
         }
-
-        String outFileName = dependenciesFileName(depGraphName, curPackageName);
-        outFile = new PrintWriter(getOutFile(outFileName));
-
-        String packageDescription = packageName.length() == 0
-            ? "the default package" : packageName;
-        addStandardHtmlProlog(outFile, "Method Dependencies for "
-            + depGraphDescription, "Method Dependencies for "
-            + depGraphDescription, "Showing Package: " + packageDescription);
       }
-      String name = method;
-      if (curClassName.compareTo(className) != 0) {
-        name = className;
-        curClassName = className;
-        outFile.println("<a name=\"" + curClassName
-            + "\"><h3 class=\"soyc-class-header\">Class: " + curClassName
-            + "</a></h3>");
+    }
+    HtmlInterner interner = new HtmlInterner();
+
+    for (String reportMethod : classesInPackage) {
+      interner.intern(getPackageSubstring(reportMethod));
+      interner.intern(getClassSubstring(reportMethod));
+      interner.intern(getMethodSubstring(reportMethod));
+
+      String depMethod = dependencies.get(reportMethod);
+      while (depMethod != null) {
+        interner.intern(getPackageSubstring(depMethod));
+        interner.intern(getClassSubstring(depMethod));
+        interner.intern(getMethodSubstring(depMethod));
+        depMethod = dependencies.get(depMethod);
       }
+    }
 
-      outFile.println("<div class='main'>");
-      outFile.println("<a class='toggle soyc-call-stack-link' onclick='toggle.call(this)'><span class='calledBy'> Call stack: </span>"
-          + name + "</a>");
-      outFile.println("<ul class=\"soyc-call-stack-list\">");
+    String outFileName = dependenciesFileName(depGraphName, packageName);
+    outFile = new PrintWriter(getOutFile(outFileName));
+    String packageDescription = packageName.length() == 0
+        ? "the default package" : packageName;
+    addStandardHtmlProlog(outFile, "Method Dependencies for "
+        + depGraphDescription,
+        "Method Dependencies for " + depGraphDescription, "Showing Package: "
+            + packageDescription);
 
+    // Write out the data values in the script
+    outFile.println("<script language=\"javascript\">");
+    outFile.println("  var internedStrings = [");
+    for (String key : interner.keySet()) {
+      outFile.println("\"" + key + "\",");
+    }
+    outFile.println("  ];");
+    // function to print a class header
+    outFile.println("  function showC(packageRef, classRef) {");
+    outFile.println("    var className = internedStrings[packageRef] + \".\" + internedStrings[classRef];");
+    outFile.println("    document.write(\"<a name='\" + className + \"'>\");");
+    outFile.println("    document.write(\"<h3 class='soyc-class-header'>Class: \" + className + \"</a></h3>\");");
+    outFile.println("  }");
+    // function to print a dependency
+    outFile.println("  function showD(c, deps) {");
+    outFile.println("    document.write(\"<div class='main'><a class='toggle soyc-call-stack-link' "
+        + "onclick='toggle.call(this)'><span class='calledBy'> Call stack: </span>\");");
+    outFile.println("    document.write(internedStrings[c[0]] + \".\" + internedStrings[c[1]] + \"::\" + "
+        + "internedStrings[c[2]] + \"</a>\");");
+    outFile.println("    document.write(\"<ul class='soyc-call-stack-list'>\");");
+    outFile.println("    for (var i = 0; i < deps.length ; i++) {");
+    outFile.println("      var s = deps[i];");
+    outFile.println("      document.write(\"<li>\" + internedStrings[s[0]] + \".\" + internedStrings[s[1]] +"
+        + "\"::\" + internedStrings[s[2]] + \"</li>\");");
+    outFile.println("    }");
+    outFile.println("    document.write(\"</ul></div>\");");
+    outFile.println("  }");
+    outFile.println("</script>");
+
+    // Write out the HTML
+    outFile.print("<script>");
+    for (String method : classesInPackage) {
+      // this key set is already in alphabetical order
+      // get the package of this method, i.e., everything up to .[A-Z]
+
+      String className = method.replaceAll("::.*", "");
       String depMethod = dependencies.get(method);
+      if (curClassName.compareTo(className) != 0) {
+        curClassName = className;
+        outFile.print("showC(" + interner.get(getPackageSubstring(className))
+            + "," + interner.get(getClassSubstring(className)) + ");");
+      }
+      String nameArray = "[" + interner.get(getPackageSubstring(method)) + ","
+          + interner.get(getClassSubstring(method)) + ","
+          + interner.get(getMethodSubstring(method)) + "]";
+      outFile.print("showD(" + nameArray + ",");
+      outFile.print(" [");
       while (depMethod != null) {
         String nextDep = dependencies.get(depMethod);
-        if (nextDep != null) {
-          outFile.println("<li>" + depMethod + "</li>");
+          // The bottom of the stack frame is not interesting.
+          if (nextDep != null) {
+          String packageString = getPackageSubstring(depMethod);
+          String classString = getClassSubstring(depMethod);
+          String methodString = getMethodSubstring(depMethod);
+          outFile.print("[" + interner.get(packageString) + ","
+              + interner.get(classString) + "," + interner.get(methodString)
+              + "]");
         }
         depMethod = nextDep;
+        if (nextDep != null) {
+          outFile.print(",");
+        }
       }
-      outFile.println("</ul>");
-      outFile.println("</div>");
+      outFile.print(" ]);");
     }
+    outFile.println("</script>");
+
     addStandardHtmlEnding(outFile);
     outFile.close();
   }
@@ -1211,7 +1356,6 @@
    *
    * @param breakdown
    * @return the name of the HTML file
-   * @throws IOException
    */
   private String makePackageHtml(SizeBreakdown breakdown) throws IOException {
     String outFileName = breakdown.getId() + "-" + getPermutationId() + "-"
diff --git a/dev/core/test/com/google/gwt/soyc/MakeTopLevelHtmlForPermTest.java b/dev/core/test/com/google/gwt/soyc/MakeTopLevelHtmlForPermTest.java
new file mode 100644
index 0000000..21578dc
--- /dev/null
+++ b/dev/core/test/com/google/gwt/soyc/MakeTopLevelHtmlForPermTest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2010 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
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.gwt.soyc;
+
+import junit.framework.TestCase;
+
+/**
+ * Test cases for {@link MakeTopLevelHtmlForPerm}.
+ */
+public class MakeTopLevelHtmlForPermTest extends TestCase {
+
+  public void testGetClassSubstring() {
+    assertEquals("myClass", MakeTopLevelHtmlForPerm.getClassSubstring("com.foo.myClass"));
+    assertEquals("myClass", MakeTopLevelHtmlForPerm.getClassSubstring("com.foo.myClass::myMethod"));
+
+    // We don't really expect these inputs, just testing to make sure they don't blow up
+    assertEquals("Empty string", "", MakeTopLevelHtmlForPerm.getClassSubstring(""));
+    assertEquals("", MakeTopLevelHtmlForPerm.getClassSubstring("::myMethod"));
+    assertEquals("", MakeTopLevelHtmlForPerm.getMethodSubstring(":"));
+    assertEquals("", MakeTopLevelHtmlForPerm.getMethodSubstring("::"));
+    assertEquals("", MakeTopLevelHtmlForPerm.getMethodSubstring("..."));
+    assertEquals("", MakeTopLevelHtmlForPerm.getMethodSubstring(".."));
+    assertEquals("", MakeTopLevelHtmlForPerm.getMethodSubstring("."));
+  }
+
+  public void testGetMethodSubstring() {
+    assertEquals("", MakeTopLevelHtmlForPerm.getMethodSubstring("com.foo.myClass"));
+    assertEquals("myMethod", MakeTopLevelHtmlForPerm.getMethodSubstring("com.foo.myClass::myMethod"));
+
+    // We don't really expect these inputs, just testing to make sure they don't blow up
+    assertEquals("Empty string", "", MakeTopLevelHtmlForPerm.getMethodSubstring(""));
+    assertEquals("myMethod", MakeTopLevelHtmlForPerm.getMethodSubstring("::myMethod"));
+    assertEquals("", MakeTopLevelHtmlForPerm.getMethodSubstring("myMethod"));
+    assertEquals("", MakeTopLevelHtmlForPerm.getMethodSubstring(":"));
+    assertEquals("", MakeTopLevelHtmlForPerm.getMethodSubstring("::"));
+  }
+
+  public void testGetPackageSubstring() {
+    assertEquals("com.foo", MakeTopLevelHtmlForPerm.getPackageSubstring("com.foo.myClass"));
+    assertEquals("com.foo", MakeTopLevelHtmlForPerm.getPackageSubstring("com.foo.myClass::myMethod"));
+
+    // We don't really expect these inputs, just testing to make sure they don't blow up
+    assertEquals("Empty string", "", MakeTopLevelHtmlForPerm.getPackageSubstring(""));
+    assertEquals("com.foo", MakeTopLevelHtmlForPerm.getPackageSubstring("com.foo.myClass::"));
+    assertEquals("com.foo", MakeTopLevelHtmlForPerm.getPackageSubstring("com.foo.myClass:"));
+    assertEquals("com", MakeTopLevelHtmlForPerm.getPackageSubstring("com"));
+    assertEquals("", MakeTopLevelHtmlForPerm.getMethodSubstring("..."));
+    assertEquals("", MakeTopLevelHtmlForPerm.getMethodSubstring(".."));
+    assertEquals("", MakeTopLevelHtmlForPerm.getMethodSubstring("."));
+  }
+}