diff --git a/evaluate.py b/evaluate.py
new file mode 100755
index 0000000000000000000000000000000000000000..7047daca48f528370513c27121e51f3de3046f54
--- /dev/null
+++ b/evaluate.py
@@ -0,0 +1,126 @@
+#!/usr/bin/env python3
+""" The jpamb evaluator
+"""
+
+import click
+import subprocess
+import sys
+from dataclasses import dataclass
+
+prim = bool | int
+
+
+@dataclass
+class Case:
+    methodid: str
+    input: str
+    result: str
+
+
+def rebuild():
+    subprocess.call(["mvn", "compile"])
+
+
+def runtime(args, enable_assertions=False, **kwargs):
+    pargs = ["java", "-cp", "target/classes/"]
+
+    if enable_assertions:
+        pargs += ["-ea"]
+
+    pargs += ["jpamb.Runtime"]
+    pargs += args
+
+    return subprocess.check_output(pargs, text=True, **kwargs)
+
+
+def getcases():
+    import csv
+
+    for r in sorted(
+        csv.reader(runtime([]).splitlines(), delimiter=" ", skipinitialspace=True)
+    ):
+        yield Case(r[0], *r[1].split(" -> "))
+
+
+@click.group
+def cli():
+    """The jpamb evaluator"""
+
+
+@cli.command
+def cases():
+    """Get a list of cases to test"""
+    for c in getcases():
+        print(c)
+
+
+@cli.command
+@click.option("--timeout", default=0.5)
+@click.argument(
+    "CMD",
+    nargs=-1,
+)
+def test(cmd, timeout):
+    """Check that all cases are valid"""
+    if not cmd:
+        cmd = ["java", "-cp", "target/classes", "-ea", "jpamb.Runtime"]
+
+    rebuild()
+
+    cases = list(getcases())
+    failed = []
+    for c in cases:
+        print(f"=" * 80)
+        print(f"{c.methodid} with {c.input}")
+        print()
+        sys.stdout.flush()
+
+        try:
+            cp = subprocess.run(
+                cmd + [c.methodid, c.input],
+                text=True,
+                stderr=sys.stdout,
+                stdout=subprocess.PIPE,
+                timeout=timeout,
+                check=True,
+            )
+            result = cp.stdout.strip()
+            success = result == c.result
+            print()
+            print(f"Got {result} which is {success}")
+        except subprocess.CalledProcessError:
+            print()
+            print(f"Process failed.")
+            success = False
+        except subprocess.TimeoutExpired:
+            success = "*" == c.result
+            print()
+            print(f"Timed out after {timeout}s which is {success}")
+        if not success:
+            failed += [c]
+        print(f"=" * 80)
+        print()
+
+    if failed:
+        print(f"Failed on {len(failed)}/{len(cases)}")
+    else:
+        print(f"Sucessfully handled {len(cases)} cases")
+
+
+@cli.command
+def evaluate():
+    """Check that all cases are valid"""
+    for c in getcases():
+        try:
+            result = runtime(
+                [c.methodid, c.input],
+                enable_assertions=True,
+                timeout=0.5,
+            ).strip()
+        except subprocess.TimeoutExpired:
+            result = "*"
+        print(c, result == c.result)
+
+
+if __name__ == "__main__":
+    cli()
diff --git a/flake.nix b/flake.nix
index a7f5adf02f092648ad75cd546e260ac5a4312b34..42f26198021bb3cec468942d6029c37c599b4410 100644
--- a/flake.nix
+++ b/flake.nix
@@ -21,7 +21,7 @@
             jdt-language-server
             jdk
             maven
-            (python3.withPackages (p: with p; []))
+            (python3.withPackages (p: with p; [click pandas]))
           ];
         };
       };
diff --git a/src/main/java/jpamb/Runtime.java b/src/main/java/jpamb/Runtime.java
index d895fa904ba11b7b1212f4f55157861e68399322..3089b525d778b3b86254b9a9282dcc8e092080bc 100644
--- a/src/main/java/jpamb/Runtime.java
+++ b/src/main/java/jpamb/Runtime.java
@@ -1,14 +1,23 @@
 package jpamb;
 
 import java.lang.reflect.*;
-import java.util.concurrent.*;
-import java.util.concurrent.atomic.AtomicReference;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.*;
 import java.util.stream.Stream;
 
 import jpamb.utils.*;
+import jpamb.utils.CaseContent.ResultType;
 import jpamb.cases.*;
 
+/**
+ * The runtime method runs a single test-case and print the result or the
+ * exeception.
+ */
 public class Runtime {
+  static List<Class<?>> caseclasses = List.of(
+      Simple.class,
+      Loops.class);
 
   public static Case[] cases(Method m) {
     var cases = m.getAnnotation(Cases.class);
@@ -22,51 +31,91 @@ public class Runtime {
     }
   }
 
-  public static void main(String[] args) throws ClassNotFoundException, InterruptedException {
-    var mths = Stream.of(Simple.class, Loops.class).flatMap(c -> Stream.of(c.getMethods())).toList();
-    for (Method m : mths) {
-      for (Case c : cases(m)) {
-        CaseContent content = CaseContent.parse(c.value());
+  public static void printType(Class<?> c, StringBuilder b) {
+    if (c.equals(void.class)) {
+      b.append("V");
+    } else if (c.equals(int.class)) {
+      b.append("I");
+    } else if (c.equals(boolean.class)) {
+      b.append("Z");
+    } else if (c.equals(double.class)) {
+      b.append("D");
+    } else if (c.equals(float.class)) {
+      b.append("F");
+    } else if (c.equals(char.class)) {
+      b.append("C");
+    } else {
+      throw new RuntimeException("Unknown type:" + c.toString());
+    }
+  }
+
+  public static String printMethodSignature(Method m) {
+    StringBuilder b = new StringBuilder();
+    b.append("(");
+    for (Class<?> c : m.getParameterTypes()) {
+      printType(c, b);
+    }
+    b.append(")");
+    printType(m.getReturnType(), b);
+    return b.toString();
+  }
 
-        if (!Modifier.isStatic(m.getModifiers())) {
-          System.out.println("Method is not static");
-          continue;
+  public static Class<?>[] parseMethodSignature(String s) {
+    Class<?>[] params = new Class[s.length()];
+    for (int i = 0; i < s.length(); i++) {
+      switch (s.charAt(i)) {
+        case 'I' -> {
+          params[i] = int.class;
+          break;
         }
+        case 'Z' -> {
+          params[i] = boolean.class;
+          break;
+        }
+      }
+    }
+    return params;
+  }
 
-        String id = m.getDeclaringClass().getName() + "." + m.getName() + content + ":";
-        System.out.printf("%-80s", id);
-        System.out.flush();
-        final AtomicReference<Throwable> atom = new AtomicReference<>();
-        Thread t = new Thread(() -> {
-          try {
-            m.invoke(null, content.params());
-          } catch (InvocationTargetException e) {
-            atom.set(e.getCause());
-          } catch (IllegalAccessException e) {
-            atom.set(e);
+  public static void main(String[] args)
+      throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException {
+    if (args.length == 0) {
+      var mths = caseclasses.stream().flatMap(c -> Stream.of(c.getMethods())).toList();
+      for (Method m : mths) {
+        for (Case c : cases(m)) {
+          CaseContent content = CaseContent.parse(c.value());
+          String sig = printMethodSignature(m);
+          String id = m.getDeclaringClass().getName() + "." + m.getName() + ":" + sig;
+          if (!Modifier.isStatic(m.getModifiers())) {
+            throw new RuntimeException("Expected " + id + " to be static");
           }
-        });
-        t.start();
-        t.join(100);
-
-        if (t.isAlive())
-          t.stop();
-
-        Throwable error = atom.get();
-        if (t.isAlive() && error == null) {
-          error = new TimeoutException();
+          System.out.printf("%-60s \"%s\"%n", id, content);
         }
-
-        String message;
-        if (error == null) {
-          message = "did not produce error";
-        } else if (content.result().expectThrows(error.getClass())) {
-          message = "success";
-        } else {
-          message = error.toString();
+      }
+      return;
+    }
+    String thecase = args[0];
+    Pattern pattern = Pattern.compile("(.*)\\.([^.(]*):\\((.*)\\)(.*)");
+    Matcher matcher = pattern.matcher(thecase);
+    if (matcher.find()) {
+      String cls = matcher.group(1);
+      String mth = matcher.group(2);
+      String prams = matcher.group(3);
+      Method m = Class.forName(cls).getMethod(mth, parseMethodSignature(prams));
+      if (!Modifier.isStatic(m.getModifiers())) {
+        throw new RuntimeException("Expected " + pattern + " to be static");
+      }
+      for (int i = 1; i < args.length; i++) {
+        Object[] params = CaseContent.parseParams(args[i]);
+        System.err.printf("Running %s with %s%n", m, Arrays.toString(params));
+        try {
+          m.invoke(null, params);
+        } catch (InvocationTargetException e) {
+          System.out.println(ResultType.fromThrowable(e.getCause()));
+          return;
         }
-        System.out.printf("%s%n", message);
       }
+      System.out.println(ResultType.SUCCESS);
     }
   }
 }
diff --git a/src/main/java/jpamb/cases/Simple.java b/src/main/java/jpamb/cases/Simple.java
index fe2fb8cca61cdef3a3858f9865f7ec71b759c977..0caf225310bd9bb679bc8a3a8508df27da445af8 100644
--- a/src/main/java/jpamb/cases/Simple.java
+++ b/src/main/java/jpamb/cases/Simple.java
@@ -35,7 +35,7 @@ public class Simple {
   }
 
   @Case("(0, 0) -> divide by zero")
-  public static double divideZeroByZero(int a, int b) {
+  public static int divideZeroByZero(int a, int b) {
     return a / b;
   }
 
diff --git a/src/main/java/jpamb/utils/CaseContent.java b/src/main/java/jpamb/utils/CaseContent.java
index 39b5299842354a42f503c5427c1e379c55b7bf83..9e0ffe1198762b595e2220837f9dd8644aa96dcb 100644
--- a/src/main/java/jpamb/utils/CaseContent.java
+++ b/src/main/java/jpamb/utils/CaseContent.java
@@ -19,16 +19,13 @@ public record CaseContent(
     return "(" + String.join(", ", sparams) + ") -> " + result.toString();
   }
 
-  public static CaseContent parse(String string) {
-    Pattern pattern = Pattern.compile("\\(([^)]*)\\)\\s*->\\s*(.+)");
-    Matcher matcher = pattern.matcher(string);
+  static Pattern paramPattern = Pattern.compile("\\(([^)]*)\\)");
 
-    // Parse the expression
+  public static Object[] parseParams(String string) {
+    Matcher matcher = paramPattern.matcher(string);
     if (matcher.find()) {
-      String args = matcher.group(1);
-      String result = matcher.group(2);
       ArrayList<Object> list = new ArrayList<>();
-      try (Scanner sc = new Scanner(args)) {
+      try (Scanner sc = new Scanner(matcher.group(1))) {
         sc.useLocale(Locale.US);
         sc.useDelimiter(" *, *");
         while (sc.hasNext()) {
@@ -39,12 +36,25 @@ public record CaseContent(
           } else {
             String var = sc.next();
             if (!var.equals(",")) {
-              throw new RuntimeException("Invalid case: " + string + " // unexpected " + var);
+              throw new RuntimeException("Invalid parameter: " + string + " // unexpected " + var);
             }
           }
         }
       }
-      return new CaseContent(list.toArray(), ResultType.parse(result));
+      return list.toArray();
+    } else {
+      throw new RuntimeException(string + " is not a paramater list");
+    }
+  }
+
+  public static CaseContent parse(String string) {
+    Pattern pattern = Pattern.compile("(\\([^)]*\\))\\s*->\\s*(.+)");
+    Matcher matcher = pattern.matcher(string);
+    // Parse the expression
+    if (matcher.find()) {
+      String args = matcher.group(1);
+      String result = matcher.group(2);
+      return new CaseContent(parseParams(args), ResultType.parse(result));
     } else {
       throw new RuntimeException("Invalid case: " + string);
     }
@@ -53,6 +63,7 @@ public record CaseContent(
   public static enum ResultType {
     DIVIDE_BY_ZERO,
     ASSERTION_ERROR,
+    SUCCESS,
     NON_TERMINATION;
 
     public static ResultType parse(String string) {
@@ -75,6 +86,8 @@ public record CaseContent(
           return "assertion error";
         case NON_TERMINATION:
           return "*";
+        case SUCCESS:
+          return "ok";
         default:
           throw new RuntimeException("Unexpected");
       }
@@ -92,5 +105,17 @@ public record CaseContent(
           throw new RuntimeException("Unexpected");
       }
     }
+
+    public static ResultType fromThrowable(Throwable cause) {
+      if (cause instanceof ArithmeticException) {
+        return DIVIDE_BY_ZERO;
+      } else if (cause instanceof AssertionError) {
+        return ASSERTION_ERROR;
+      } else if (cause instanceof TimeoutException) {
+        return NON_TERMINATION;
+      } else {
+        throw new RuntimeException("Unexpected");
+      }
+    }
   }
 }