Add test for signature attribute parsing

Ensure that the signature parser always throws
GenericSignatureFormatError when there is a parsing error or when the
parser action handler throws.

Bug: 80029761
Change-Id: Iec0d29275b05a19df25f71467b90b73fde257916
diff --git a/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureParser.java b/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureParser.java
index 0cc0d53..5b549a3 100644
--- a/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureParser.java
+++ b/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureParser.java
@@ -75,24 +75,51 @@
   }
 
   public void parseClassSignature(String signature) {
-    actions.start();
-    setInput(signature);
-    parseClassSignature();
-    actions.stop();
+    try {
+      actions.start();
+      setInput(signature);
+      parseClassSignature();
+      actions.stop();
+    } catch (GenericSignatureFormatError e) {
+      throw e;
+    } catch (Throwable t) {
+      Error e = new GenericSignatureFormatError(
+          "Unknown error parsing generic signature: " + t.getMessage());
+      e.addSuppressed(t);
+      throw e;
+    }
   }
 
   public void parseMethodSignature(String signature) {
-    actions.start();
-    setInput(signature);
-    parseMethodTypeSignature();
-    actions.stop();
+    try {
+      actions.start();
+      setInput(signature);
+      parseMethodTypeSignature();
+      actions.stop();
+    } catch (GenericSignatureFormatError e) {
+      throw e;
+    } catch (Throwable t) {
+      Error e = new GenericSignatureFormatError(
+          "Unknown error parsing generic signature: " + t.getMessage());
+      e.addSuppressed(t);
+      throw e;
+    }
   }
 
   public void parseFieldSignature(String signature) {
-    actions.start();
-    setInput(signature);
-    parseFieldTypeSignature();
-    actions.stop();
+    try {
+      actions.start();
+      setInput(signature);
+      parseFieldTypeSignature();
+      actions.stop();
+  } catch (GenericSignatureFormatError e) {
+    throw e;
+  } catch (Throwable t) {
+    Error e = new GenericSignatureFormatError(
+        "Unknown error parsing generic signature: " + t.getMessage());
+    e.addSuppressed(t);
+    throw e;
+  }
   }
 
   private void setInput(String input) {
@@ -178,7 +205,7 @@
         updateTypeVariableSignature();
         break;
       default:
-        parseError();
+        parseError("Expected L, [ or T", pos);
     }
   }
 
@@ -341,15 +368,18 @@
         eof = true;
       }
     } else {
-      parseError("Unexpected end of signature");
+      parseError("Unexpected end of signature", pos);
     }
   }
 
   private void expect(char c) {
+    if (eof) {
+      parseError("Unexpected end of signature", pos);
+    }
     if (symbol == c) {
       scanSymbol();
     } else {
-      parseError("Expected " + c);
+      parseError("Expected " + c, pos - 1);
     }
   }
 
@@ -369,7 +399,7 @@
   // PRE: symbol is the first char of the identifier.
   // POST: symbol = the next symbol AFTER the identifier.
   private void scanIdentifier() {
-    if (!eof) {
+    if (!eof && pos < buffer.length) {
       StringBuilder identBuf = new StringBuilder(32);
       if (!isStopSymbol(symbol)) {
         identBuf.append(symbol);
@@ -399,15 +429,15 @@
         parseError();
       }
     } else {
-      parseError("Unexpected end of signature");
+      parseError("Unexpected end of signature", pos);
     }
   }
 
   private void parseError() {
-    parseError("Unexpected");
+    parseError("Unexpected", pos);
   }
 
-  private void parseError(String message) {
+  private void parseError(String message, int pos) {
     String arrow = CharBuffer.allocate(pos).toString().replace('\0', ' ') + '^';
     throw new GenericSignatureFormatError(
         message + " at position " + (pos + 1) + "\n" + String.valueOf(buffer) + "\n" + arrow);
diff --git a/src/test/java/com/android/tools/r8/naming/GenericSignatureParserTest.java b/src/test/java/com/android/tools/r8/naming/GenericSignatureParserTest.java
new file mode 100644
index 0000000..eebb19a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/GenericSignatureParserTest.java
@@ -0,0 +1,472 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.naming;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.naming.signature.GenericSignatureAction;
+import com.android.tools.r8.naming.signature.GenericSignatureParser;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.lang.reflect.GenericSignatureFormatError;
+import java.util.List;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import org.junit.Test;
+
+public class GenericSignatureParserTest extends TestBase {
+  private static class ReGenerateGenericSignatureRewriter
+      implements GenericSignatureAction<String> {
+
+    private StringBuilder renamedSignature;
+
+    public String getRenamedSignature() {
+      return renamedSignature.toString();
+    }
+
+    @Override
+    public void parsedSymbol(char symbol) {
+      renamedSignature.append(symbol);
+    }
+
+    @Override
+    public void parsedIdentifier(String identifier) {
+      renamedSignature.append(identifier);
+    }
+
+    @Override
+    public String parsedTypeName(String name) {
+      renamedSignature.append(name);
+      return name;
+    }
+
+    @Override
+    public String parsedInnerTypeName(String enclosingType, String name) {
+      renamedSignature.append(name);
+      return name;
+    }
+
+    @Override
+    public void start() {
+      renamedSignature = new StringBuilder();
+    }
+
+    @Override
+    public void stop() {
+      // nothing to do
+    }
+  }
+
+  public void parseSimpleError(BiConsumer<GenericSignatureParser<String>, String> parse,
+      Consumer<GenericSignatureFormatError> errorChecker) {
+    try {
+      String signature = "X";
+      GenericSignatureParser<String> parser =
+          new GenericSignatureParser<>(new ReGenerateGenericSignatureRewriter());
+      parse.accept(parser, signature);
+      fail("Succesfully parsed " + signature);
+    } catch (GenericSignatureFormatError e) {
+      errorChecker.accept(e);
+    }
+  }
+
+  @Test
+  public void simpleParseError() {
+    parseSimpleError(
+        GenericSignatureParser::parseClassSignature,
+        e -> assertTrue(e.getMessage().startsWith("Expected L at position 1")));
+    // TODO(sgjesse): The position 2 reported here is onr off.
+    parseSimpleError(
+        GenericSignatureParser::parseFieldSignature,
+        e -> assertTrue(e.getMessage().startsWith("Expected L, [ or T at position 2")));
+    parseSimpleError(GenericSignatureParser::parseMethodSignature,
+        e -> assertTrue(e.getMessage().startsWith("Expected ( at position 1")));
+  }
+
+  private void parseSignature(String signature, Set<Integer> validPrefixes,
+      Consumer<String> parser, ReGenerateGenericSignatureRewriter rewriter) {
+    for (int i = 0; i < 2; i++) {
+      parser.accept(signature);
+      assertEquals(signature, rewriter.getRenamedSignature());
+    }
+
+    for (int i = 1; i < signature.length(); i++) {
+      try {
+        if (validPrefixes == null || !validPrefixes.contains(i)) {
+          parser.accept(signature.substring(0, i));
+          fail("Succesfully parsed " + signature.substring(0, i) + " (position " + i +")");
+        }
+      } catch (GenericSignatureFormatError e) {
+        assertTrue("" + i + " Was: " + e.getMessage(), e.getMessage().contains("at position " + (i + 1)));
+      }
+    }
+  }
+
+  private void parseClassSignature(String signature, Set<Integer> validPrefixes) {
+    ReGenerateGenericSignatureRewriter rewriter = new ReGenerateGenericSignatureRewriter();
+    GenericSignatureParser<String> parser = new GenericSignatureParser<>(rewriter);
+
+    parseSignature(signature, validPrefixes, parser::parseClassSignature, rewriter);
+  }
+
+  private void parseFieldSignature(String signature, Set<Integer> validPrefixes) {
+    ReGenerateGenericSignatureRewriter rewriter = new ReGenerateGenericSignatureRewriter();
+    GenericSignatureParser<String> parser = new GenericSignatureParser<>(rewriter);
+
+    parseSignature(signature, validPrefixes, parser::parseFieldSignature, rewriter);
+  }
+
+  private void parseMethodSignature(String signature, Set<Integer> validPrefixes) {
+    ReGenerateGenericSignatureRewriter rewriter = new ReGenerateGenericSignatureRewriter();
+    GenericSignatureParser<String> parser = new GenericSignatureParser<>(rewriter);
+
+    parseSignature(signature, validPrefixes, parser::parseMethodSignature, rewriter);
+  }
+
+  public void forClassSignatures(BiConsumer<String, Set<Integer>> consumer) {
+    consumer.accept("Ljava/lang/Object;", null);
+    consumer.accept("LOuter$InnerInterface<TU;>;", null);
+
+    consumer.accept("La;", null);
+    consumer.accept("La.i;", null);
+    consumer.accept("La.i.j;", null);
+    consumer.accept("La/b;", null);
+    consumer.accept("La/b.i;", null);
+    consumer.accept("La/b.i.j;", null);
+    consumer.accept("La/b/c;", null);
+    consumer.accept("La/b/c.i;", null);
+    consumer.accept("La/b/c.i.j;", null);
+    consumer.accept("La$b;", null);
+    consumer.accept("La$b.i;", null);
+    consumer.accept("La$b.i.j;", null);
+    consumer.accept("La$b$c;", null);
+    consumer.accept("La$b$c.i;", null);
+    consumer.accept("La$b$c.i.j;", null);
+
+    StringBuilder builder = new StringBuilder();
+    for (int i = 0; i < 3; i++) {
+      builder.append("TT;");
+      consumer.accept("La<" + builder.toString() + ">;", null);
+      consumer.accept("La<" + builder.toString() + ">.i;", null);
+      consumer.accept("La<" + builder.toString() + ">.i.j;", null);
+      consumer.accept("La/b<" + builder.toString() + ">;", null);
+      consumer.accept("La/b<" + builder.toString() + ">.i;", null);
+      consumer.accept("La/b<" + builder.toString() + ">.i.j;", null);
+      consumer.accept("La/b/c<" + builder.toString() + ">;", null);
+      consumer.accept("La/b/c<" + builder.toString() + ">.i;", null);
+      consumer.accept("La/b/c<" + builder.toString() + ">.i.j;", null);
+      consumer.accept("La$b<" + builder.toString() + ">;", null);
+      consumer.accept("La$b<" + builder.toString() + ">.i;", null);
+      consumer.accept("La$b<" + builder.toString() + ">.i.j;", null);
+      consumer.accept("La$b$c<" + builder.toString() + ">;", null);
+      consumer.accept("La$b$c<" + builder.toString() + ">.i;", null);
+      consumer.accept("La$b$c<" + builder.toString() + ">.i.j;", null);
+    }
+  }
+
+
+  public void forBasicTypes(Consumer<String> consumer) {
+    for (char c : "BCDFIJSZ".toCharArray()) {
+      consumer.accept(new String(new char[]{c}));
+    }
+  }
+
+  public void forBasicTypesAndVoid(Consumer<String> consumer) {
+    forBasicTypes(consumer);
+    consumer.accept("V");
+  }
+
+  public void forTypeVariableSignatures(BiConsumer<String, Set<Integer>> consumer) {
+    consumer.accept("TT;", null);
+
+    consumer.accept("Ta;", null);
+    consumer.accept("Tab;", null);
+    consumer.accept("Tabc;", null);
+    consumer.accept("Ta-b;", null);
+    consumer.accept("Ta-b-c;", null);
+  }
+
+  public void forArrayTypeSignatures(BiConsumer<String, Set<Integer>> consumer) {
+    StringBuilder arrayPrefix = new StringBuilder();
+    for (int i = 0; i < 3; i++) {
+      arrayPrefix.append("[");
+      forBasicTypes(t -> consumer.accept(arrayPrefix.toString() + t, null));
+      forClassSignatures((x, y) -> {
+        consumer.accept(arrayPrefix.toString() + x, y);
+      });
+      forTypeVariableSignatures((x, y) -> {
+        consumer.accept(arrayPrefix.toString() + x, y);
+      });
+      //consumer.accept();
+    }
+  }
+
+  public void forClassTypeSignatures(BiConsumer<String, Set<Integer>> consumer) {
+    // In https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3.4 (Java 7) it
+    // says: "If the class bound does not specify a type, it is taken to be Object.". That sentence
+    // is not present for Java 8 or Java 9. We do test it here, and javac from OpenJDK 8 will also
+    // produce that for a class that extends Object and implements at least one interface.
+
+    // class C<T> { ... }.
+    consumer.accept("<T:>Ljava/lang/Object;", null);
+    consumer.accept("<T:Ljava/lang/Object;>Ljava/lang/Object;", null);
+
+    // class C<T,U> { ... }.
+    consumer.accept("<T:U:>Ljava/lang/Object;", null);
+    consumer.accept("<T:Ljava/lang/Object;U:Ljava/lang/Object;>Ljava/lang/Object;", null);
+
+    // class C<T extends AbstractList> { ... }.
+    consumer.accept("<T:Ljava/util/AbstractList;>Ljava/lang/Object;", null);
+    // class C<T extends AbstractList<T>> { ... }.
+    consumer.accept("<T:Ljava/util/AbstractList<TT;>;>Ljava/lang/Object;", null);
+
+    // class C<T extends List> { ... }.
+    consumer.accept("<T::Ljava/util/List;>Ljava/lang/Object;", null);
+    // class C<T extends List<T>> { ... }.
+    consumer.accept("<T::Ljava/util/List<TT;>;>Ljava/lang/Object;", null);
+    // class C<T extends List<T[]>> { ... }.
+    consumer.accept("<T::Ljava/util/List<[TT;>;>Ljava/lang/Object;", null);
+    // class C<T extends List<T[][]>> { ... }.
+    consumer.accept("<T::Ljava/util/List<[[TT;>;>Ljava/lang/Object;", null);
+
+    // class C<T extends AbstractList & List> { ... }.
+    consumer.accept("<T:Ljava/util/AbstractList;:Ljava/util/List;>Ljava/lang/Object;", null);
+    // class C<T extends AbstractList<T> & List<T>> { ... }.
+    consumer.accept(
+        "<T:Ljava/util/AbstractList<TT;>;:Ljava/util/List<TT;>;>Ljava/lang/Object;", null);
+    // class C<T extends AbstractList<T[]> & List<T[]>> { ... }.
+    consumer.accept(
+        "<T:Ljava/util/AbstractList<[TT;>;:Ljava/util/List<[TT;>;>Ljava/lang/Object;", null);
+    // class C<T extends AbstractList<T[][]> & List<T[][]>> { ... }.
+    consumer.accept(
+        "<T:Ljava/util/AbstractList<[[TT;>;:Ljava/util/List<[[TT;>;>Ljava/lang/Object;", null);
+
+    // class C<T extends AbstractList & List & Iterator> { ... }.
+    consumer.accept(
+        "<T:Ljava/util/AbstractList;:Ljava/util/List;:Ljava/util/Iterator;>Ljava/lang/Object;",
+        null);
+    // class C<T extends AbstractList<T> & List<T> & Iterator<T>> { ... }.
+    consumer.accept(
+        "<T:Ljava/util/AbstractList<TT;>;:Ljava/util/List<TT;>;:Ljava/util/Iterator<TT;>;>"
+            + "Ljava/lang/Object;", null);
+
+    // class C<T,U> { ... }.
+    consumer.accept("<T:U:>Ljava/lang/Object;", null);
+    consumer.accept("<T:Ljava/lang/Object;U:Ljava/lang/Object;>Ljava/lang/Object;", null);
+
+    // class C extends java.util.AbstractList<String>
+    consumer.accept("Ljava/util/AbstractList<Ljava/lang/String;>;", null);
+    // class C extends java.util.AbstractList<String> implements List<String>
+    consumer.accept(
+        "Ljava/util/AbstractList<Ljava/lang/String;>;Ljava/util/List<Ljava/lang/String;>;",
+        ImmutableSet.of(44));
+    // class C extends java.util.AbstractList<String> implements List<String>, Iterator<String>
+    consumer.accept(
+        "Ljava/util/AbstractList<Ljava/lang/String;>;"
+            + "Ljava/util/List<Ljava/lang/String;>;Ljava/util/Iterator<Ljava/lang/String;>;",
+        ImmutableSet.of(44, 80));
+
+    // class C<T> extends java.util.AbstractList<T>
+    consumer.accept("<T:Ljava/lang/Object;>Ljava/util/AbstractList<TT;>;", null);
+    // class C<T> extends java.util.AbstractList<T> implements List<T>
+    consumer.accept("<T:Ljava/lang/Object;>Ljava/util/AbstractList<TT;>;Ljava/util/List<TT;>;",
+        ImmutableSet.of(51));
+    // class C<T> extends java.util.AbstractList<T> implements List<T>, Iterator<T>
+    consumer.accept("<T:Ljava/lang/Object;>Ljava/util/AbstractList<TT;>"
+            + ";Ljava/util/List<TT;>;Ljava/util/Iterator<TT;>;",
+        ImmutableSet.of(51, 72));
+
+    // class Outer<T> {
+    //   class Inner {
+    //     class InnerInner {
+    //     }
+    //     class ExtendsInnerInner extends InnerInner {
+    //     }
+    //   }
+    //   class ExtendsInner extends Inner {
+    //   }
+    // }
+    consumer.accept("<T:Ljava/lang/Object;>Ljava/lang/Object;", null);  // Outer signature.
+    // Inner has no signature.
+    // InnerInner has no signature.
+    consumer.accept("LOuter<TT;>.Inner.InnerInner;", null);  // ExtendsInnerInner signature.
+    consumer.accept("LOuter<TT;>.Inner;", null);  // ExtendsInner signature.
+
+    // class Outer<T> {
+    //   class Inner<T> {
+    //   }
+    //   interface InnerInterface<T> {
+    //   }
+    //   abstract class ExtendsInner<U> extends Inner implements InnerInterface<U> {
+    //   }
+    // }
+    consumer.accept(
+        "<U:Ljava/lang/Object;>LOuter<TT;>.Inner<TT;>;LOuter$InnerInterface<TU;>;",
+        ImmutableSet.of(45));
+  }
+
+  public void forMethodSignatures(BiConsumer<String, Set<Integer>> consumer) {
+    forBasicTypesAndVoid(t -> consumer.accept("()" + t, null));
+    forBasicTypesAndVoid(t -> consumer.accept("(BCDFIJSZ)" + t, null));
+    forBasicTypesAndVoid(t -> consumer.accept("<T:>(BCDFIJSZ)" + t, null));
+    forBasicTypesAndVoid(t -> consumer.accept("<T:Ljava/util/List;>(BCDFIJSZ)" + t, null));
+    forBasicTypesAndVoid(t -> consumer.accept("<T:U:>(BCDFIJSZ)" + t, null));
+    forBasicTypesAndVoid(
+        t -> consumer.accept("<T:Ljava/util/List;U:Ljava/util/List;>(BCDFIJSZ)" + t, null));
+
+    consumer.accept("<T:U:>(Ljava/util/List<TT;>;Ljava/util/Iterator<TT;>;)V", null);
+    consumer.accept(
+        "<T:U:>(Ljava/util/List<TT;>;Ljava/util/Iterator<TT;>;)Ljava/util/List<TT;>;", null);
+
+    consumer.accept("<T:U:>(La/b/c<TT;>.i<TT;>;)V", null);
+    consumer.accept("<T:U:>(La/b/c<TT;>.i<TT;>.j<TU;>;)V", null);
+
+    consumer.accept("<T:>()La/b/c<TT;>;", null);
+    consumer.accept("<T:>()La/b/c<TT;>.i<TT;>;", null);
+    consumer.accept("<T:>()La/b/c<TT;>.i<TT;>.j<TT;>;", null);
+  }
+
+  @Test
+  public void testClassSignatureGenericClass() {
+    forClassTypeSignatures(this::parseClassSignature);
+  }
+
+  @Test
+  public void parseFieldSignature() {
+    forClassSignatures(this::parseFieldSignature);
+    forTypeVariableSignatures(this::parseFieldSignature);
+    forArrayTypeSignatures(this::parseFieldSignature);
+  }
+
+  @Test
+  public void parseMethodSignature() {
+    forMethodSignatures(this::parseMethodSignature);
+  }
+
+  private void failingParseAction(Consumer<GenericSignatureParser<String>> parse)
+      throws Exception {
+    class ThrowsInParserActionBase<E extends Error> extends ReGenerateGenericSignatureRewriter {
+      protected Supplier<? extends E> exceptionSupplier;
+
+      private ThrowsInParserActionBase(Supplier<? extends E> exceptionSupplier) {
+        this.exceptionSupplier = exceptionSupplier;
+      }
+    }
+
+    class ThrowsInParsedSymbol<E extends Error> extends ThrowsInParserActionBase<E> {
+      private ThrowsInParsedSymbol(Supplier<? extends E> exceptionSupplier) {
+        super(exceptionSupplier);
+      }
+
+      @Override
+      public void parsedSymbol(char symbol) {
+        throw exceptionSupplier.get();
+      }
+    }
+
+    class ThrowsInParsedIdentifier<E extends Error> extends ThrowsInParserActionBase<E> {
+      private ThrowsInParsedIdentifier(Supplier<? extends E> exceptionSupplier) {
+        super(exceptionSupplier);
+      }
+
+      @Override
+      public void parsedIdentifier(String identifier) {
+        throw exceptionSupplier.get();
+      }
+    }
+
+    class ThrowsInParsedTypeName<E extends Error> extends ThrowsInParserActionBase<E> {
+      private ThrowsInParsedTypeName(Supplier<? extends E> exceptionSupplier) {
+        super(exceptionSupplier);
+      }
+
+      @Override
+      public String parsedTypeName(String name) {
+        throw exceptionSupplier.get();
+      }
+    }
+
+    class ThrowsInParsedInnerTypeName<E extends Error> extends ThrowsInParserActionBase<E> {
+      private ThrowsInParsedInnerTypeName(Supplier<? extends E> exceptionSupplier) {
+        super(exceptionSupplier);
+      }
+
+      @Override
+      public String parsedInnerTypeName(String enclosingType, String name) {
+        throw exceptionSupplier.get();
+      }
+    }
+
+    class ThrowsInStart<E extends Error> extends ThrowsInParserActionBase<E> {
+      private ThrowsInStart(Supplier<? extends E> exceptionSupplier) {
+        super(exceptionSupplier);
+      }
+
+      @Override
+      public void start() {
+        throw exceptionSupplier.get();
+      }
+    }
+
+    class ThrowsInStop<E extends Error> extends ThrowsInParserActionBase<E> {
+      private ThrowsInStop(Supplier<? extends E> exceptionSupplier) {
+        super(exceptionSupplier);
+      }
+
+      @Override
+      public void stop() {
+        throw exceptionSupplier.get();
+      }
+    }
+
+    List<GenericSignatureAction<String>> throwingActions = ImmutableList.of(
+        new ThrowsInParsedSymbol<>(() -> new GenericSignatureFormatError("ERROR")),
+        new ThrowsInParsedSymbol<>(() -> new Error("ERROR")),
+        new ThrowsInParsedIdentifier<>(() -> new GenericSignatureFormatError("ERROR")),
+        new ThrowsInParsedIdentifier<>(() -> new Error("ERROR")),
+        new ThrowsInParsedTypeName<>(() -> new GenericSignatureFormatError("ERROR")),
+        new ThrowsInParsedTypeName<>(() -> new Error("ERROR")),
+        new ThrowsInParsedInnerTypeName<>(() -> new GenericSignatureFormatError("ERROR")),
+        new ThrowsInParsedInnerTypeName<>(() -> new Error("ERROR")),
+        new ThrowsInStart<>(() -> new GenericSignatureFormatError("ERROR")),
+        new ThrowsInStart<>(() -> new Error("ERROR")),
+        new ThrowsInStop<>(() -> new GenericSignatureFormatError("ERROR")),
+        new ThrowsInStop<>(() -> new Error("ERROR")));
+
+    int plainErrorCount = 0;
+    for (GenericSignatureAction<String> action : throwingActions) {
+      GenericSignatureParser<String> parser = new GenericSignatureParser<>(action);
+      try {
+        // This class signature hits all action callbacks.
+        parse.accept(parser);
+        fail("Parse succeeded for " + action.getClass().getSimpleName());
+      } catch (GenericSignatureFormatError e) {
+        if (e.getSuppressed().length == 0) {
+          assertEquals("ERROR", e.getMessage());
+        } else {
+          plainErrorCount++;
+          assertEquals("Unknown error parsing generic signaure: ERROR", e.getMessage());
+        }
+      }
+    }
+    assertEquals(6, plainErrorCount);
+  }
+
+  @Test
+  public void failingParseAction() throws Exception {
+    // These signatures hits all action callbacks.
+    failingParseAction(parser -> parser.parseClassSignature(
+        "<U:Ljava/lang/Object;>LOuter<TT;>.Inner;Ljava/util/List<TU;>;"));
+    failingParseAction(
+        parser -> parser.parseFieldSignature("LOuter$InnerInterface<TU;>.Inner;"));
+    failingParseAction(
+        parser -> parser.parseMethodSignature("(LOuter$InnerInterface<TU;>.Inner;)V"));
+  }
+}