aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/java/org/zwobble/couscous/Project.java7
-rw-r--r--src/main/java/org/zwobble/couscous/ast/Assignment.java16
-rw-r--r--src/main/java/org/zwobble/couscous/ast/CallNode.java5
-rw-r--r--src/main/java/org/zwobble/couscous/ast/ClassNode.java14
-rw-r--r--src/main/java/org/zwobble/couscous/ast/ExpressionNode.java7
-rw-r--r--src/main/java/org/zwobble/couscous/ast/ExpressionStatementNode.java15
-rw-r--r--src/main/java/org/zwobble/couscous/ast/FormalArgumentNode.java9
-rw-r--r--src/main/java/org/zwobble/couscous/ast/LiteralNode.java26
-rw-r--r--src/main/java/org/zwobble/couscous/ast/MethodNode.java22
-rw-r--r--src/main/java/org/zwobble/couscous/ast/ReturnNode.java15
-rw-r--r--src/main/java/org/zwobble/couscous/ast/StatementNode.java7
-rw-r--r--src/main/java/org/zwobble/couscous/ast/VariableNode.java5
-rw-r--r--src/main/java/org/zwobble/couscous/ast/VariableReferenceNode.java19
-rw-r--r--src/main/java/org/zwobble/couscous/ast/visitors/ExpressionNodeVisitor.java11
-rw-r--r--src/main/java/org/zwobble/couscous/ast/visitors/StatementNodeVisitor.java9
-rw-r--r--src/main/java/org/zwobble/couscous/interpreter/Interpreter.java110
-rw-r--r--src/main/java/org/zwobble/couscous/interpreter/InterpreterException.java6
-rw-r--r--src/main/java/org/zwobble/couscous/interpreter/WrongNumberOfArguments.java13
-rw-r--r--src/main/java/org/zwobble/couscous/values/IntegerValue.java8
-rw-r--r--src/main/java/org/zwobble/couscous/values/InterpreterValue.java5
-rw-r--r--src/main/java/org/zwobble/couscous/values/StringValue.java8
-rw-r--r--src/main/java/org/zwobble/couscous/values/UnitValue.java11
-rw-r--r--src/test/java/org/zwobble/couscous/tests/interpreter/InterpreterTests.java117
-rw-r--r--src/test/java/org/zwobble/couscous/tests/util/ExtraAsserts.java21
24 files changed, 486 insertions, 0 deletions
diff --git a/src/main/java/org/zwobble/couscous/Project.java b/src/main/java/org/zwobble/couscous/Project.java
new file mode 100644
index 0000000..6afac6e
--- /dev/null
+++ b/src/main/java/org/zwobble/couscous/Project.java
@@ -0,0 +1,7 @@
+package org.zwobble.couscous;
+
+import org.zwobble.couscous.ast.ClassNode;
+
+public interface Project {
+ ClassNode findClass(String name);
+}
diff --git a/src/main/java/org/zwobble/couscous/ast/Assignment.java b/src/main/java/org/zwobble/couscous/ast/Assignment.java
new file mode 100644
index 0000000..aa7f89b
--- /dev/null
+++ b/src/main/java/org/zwobble/couscous/ast/Assignment.java
@@ -0,0 +1,16 @@
+package org.zwobble.couscous.ast;
+
+import org.zwobble.couscous.ast.visitors.ExpressionNodeVisitor;
+
+import lombok.Value;
+
+@Value
+public class Assignment implements ExpressionNode {
+ VariableReferenceNode target;
+ ExpressionNode value;
+
+ @Override
+ public <T> T accept(ExpressionNodeVisitor<T> visitor) {
+ return visitor.visit(this);
+ }
+}
diff --git a/src/main/java/org/zwobble/couscous/ast/CallNode.java b/src/main/java/org/zwobble/couscous/ast/CallNode.java
new file mode 100644
index 0000000..d71c891
--- /dev/null
+++ b/src/main/java/org/zwobble/couscous/ast/CallNode.java
@@ -0,0 +1,5 @@
+package org.zwobble.couscous.ast;
+
+public class CallNode {
+
+}
diff --git a/src/main/java/org/zwobble/couscous/ast/ClassNode.java b/src/main/java/org/zwobble/couscous/ast/ClassNode.java
new file mode 100644
index 0000000..ffa5165
--- /dev/null
+++ b/src/main/java/org/zwobble/couscous/ast/ClassNode.java
@@ -0,0 +1,14 @@
+package org.zwobble.couscous.ast;
+
+import java.util.List;
+
+import lombok.Builder;
+import lombok.Singular;
+import lombok.Value;
+
+@Builder
+@Value
+public class ClassNode {
+ @Singular
+ List<MethodNode> methods;
+}
diff --git a/src/main/java/org/zwobble/couscous/ast/ExpressionNode.java b/src/main/java/org/zwobble/couscous/ast/ExpressionNode.java
new file mode 100644
index 0000000..097377a
--- /dev/null
+++ b/src/main/java/org/zwobble/couscous/ast/ExpressionNode.java
@@ -0,0 +1,7 @@
+package org.zwobble.couscous.ast;
+
+import org.zwobble.couscous.ast.visitors.ExpressionNodeVisitor;
+
+public interface ExpressionNode {
+ <T> T accept(ExpressionNodeVisitor<T> visitor);
+}
diff --git a/src/main/java/org/zwobble/couscous/ast/ExpressionStatementNode.java b/src/main/java/org/zwobble/couscous/ast/ExpressionStatementNode.java
new file mode 100644
index 0000000..ec04663
--- /dev/null
+++ b/src/main/java/org/zwobble/couscous/ast/ExpressionStatementNode.java
@@ -0,0 +1,15 @@
+package org.zwobble.couscous.ast;
+
+import org.zwobble.couscous.ast.visitors.StatementNodeVisitor;
+
+import lombok.Value;
+
+@Value
+public class ExpressionStatementNode implements StatementNode {
+ ExpressionNode expression;
+
+ @Override
+ public <T> T accept(StatementNodeVisitor<T> visitor) {
+ return visitor.visit(this);
+ }
+}
diff --git a/src/main/java/org/zwobble/couscous/ast/FormalArgumentNode.java b/src/main/java/org/zwobble/couscous/ast/FormalArgumentNode.java
new file mode 100644
index 0000000..da3a838
--- /dev/null
+++ b/src/main/java/org/zwobble/couscous/ast/FormalArgumentNode.java
@@ -0,0 +1,9 @@
+package org.zwobble.couscous.ast;
+
+import lombok.Value;
+
+@Value
+public class FormalArgumentNode implements VariableNode {
+ int id;
+ String name;
+}
diff --git a/src/main/java/org/zwobble/couscous/ast/LiteralNode.java b/src/main/java/org/zwobble/couscous/ast/LiteralNode.java
new file mode 100644
index 0000000..38688d8
--- /dev/null
+++ b/src/main/java/org/zwobble/couscous/ast/LiteralNode.java
@@ -0,0 +1,26 @@
+package org.zwobble.couscous.ast;
+
+import org.zwobble.couscous.ast.visitors.ExpressionNodeVisitor;
+import org.zwobble.couscous.values.IntegerValue;
+import org.zwobble.couscous.values.InterpreterValue;
+import org.zwobble.couscous.values.StringValue;
+
+import lombok.Value;
+
+@Value
+public class LiteralNode implements ExpressionNode {
+ public static LiteralNode of(String value) {
+ return new LiteralNode(new StringValue(value));
+ }
+
+ public static LiteralNode of(int value) {
+ return new LiteralNode(new IntegerValue(value));
+ }
+
+ InterpreterValue value;
+
+ @Override
+ public <T> T accept(ExpressionNodeVisitor<T> visitor) {
+ return visitor.visit(this);
+ }
+}
diff --git a/src/main/java/org/zwobble/couscous/ast/MethodNode.java b/src/main/java/org/zwobble/couscous/ast/MethodNode.java
new file mode 100644
index 0000000..91acbd5
--- /dev/null
+++ b/src/main/java/org/zwobble/couscous/ast/MethodNode.java
@@ -0,0 +1,22 @@
+package org.zwobble.couscous.ast;
+
+import java.util.List;
+
+import lombok.Builder;
+import lombok.Singular;
+import lombok.Value;
+
+@Builder
+@Value
+public class MethodNode {
+ public static MethodNodeBuilder staticMethod(String name) {
+ return builder().isStatic(true).name(name);
+ }
+
+ boolean isStatic;
+ String name;
+ @Singular
+ List<FormalArgumentNode> arguments;
+ @Singular("statement")
+ List<StatementNode> body;
+}
diff --git a/src/main/java/org/zwobble/couscous/ast/ReturnNode.java b/src/main/java/org/zwobble/couscous/ast/ReturnNode.java
new file mode 100644
index 0000000..529dbb5
--- /dev/null
+++ b/src/main/java/org/zwobble/couscous/ast/ReturnNode.java
@@ -0,0 +1,15 @@
+package org.zwobble.couscous.ast;
+
+import org.zwobble.couscous.ast.visitors.StatementNodeVisitor;
+
+import lombok.Value;
+
+@Value
+public class ReturnNode implements StatementNode {
+ ExpressionNode value;
+
+ @Override
+ public <T> T accept(StatementNodeVisitor<T> visitor) {
+ return visitor.visit(this);
+ }
+}
diff --git a/src/main/java/org/zwobble/couscous/ast/StatementNode.java b/src/main/java/org/zwobble/couscous/ast/StatementNode.java
new file mode 100644
index 0000000..773420e
--- /dev/null
+++ b/src/main/java/org/zwobble/couscous/ast/StatementNode.java
@@ -0,0 +1,7 @@
+package org.zwobble.couscous.ast;
+
+import org.zwobble.couscous.ast.visitors.StatementNodeVisitor;
+
+public interface StatementNode {
+ <T> T accept(StatementNodeVisitor<T> visitor);
+}
diff --git a/src/main/java/org/zwobble/couscous/ast/VariableNode.java b/src/main/java/org/zwobble/couscous/ast/VariableNode.java
new file mode 100644
index 0000000..4c689c5
--- /dev/null
+++ b/src/main/java/org/zwobble/couscous/ast/VariableNode.java
@@ -0,0 +1,5 @@
+package org.zwobble.couscous.ast;
+
+public interface VariableNode {
+ int getId();
+}
diff --git a/src/main/java/org/zwobble/couscous/ast/VariableReferenceNode.java b/src/main/java/org/zwobble/couscous/ast/VariableReferenceNode.java
new file mode 100644
index 0000000..536a7c3
--- /dev/null
+++ b/src/main/java/org/zwobble/couscous/ast/VariableReferenceNode.java
@@ -0,0 +1,19 @@
+package org.zwobble.couscous.ast;
+
+import org.zwobble.couscous.ast.visitors.ExpressionNodeVisitor;
+
+import lombok.Value;
+
+@Value
+public class VariableReferenceNode implements ExpressionNode {
+ public static VariableReferenceNode reference(VariableNode variable) {
+ return new VariableReferenceNode(variable.getId());
+ }
+
+ int referentId;
+
+ @Override
+ public <T> T accept(ExpressionNodeVisitor<T> visitor) {
+ return visitor.visit(this);
+ }
+}
diff --git a/src/main/java/org/zwobble/couscous/ast/visitors/ExpressionNodeVisitor.java b/src/main/java/org/zwobble/couscous/ast/visitors/ExpressionNodeVisitor.java
new file mode 100644
index 0000000..0167abc
--- /dev/null
+++ b/src/main/java/org/zwobble/couscous/ast/visitors/ExpressionNodeVisitor.java
@@ -0,0 +1,11 @@
+package org.zwobble.couscous.ast.visitors;
+
+import org.zwobble.couscous.ast.Assignment;
+import org.zwobble.couscous.ast.LiteralNode;
+import org.zwobble.couscous.ast.VariableReferenceNode;
+
+public interface ExpressionNodeVisitor<T> {
+ T visit(LiteralNode literal);
+ T visit(VariableReferenceNode variableReference);
+ T visit(Assignment assignment);
+}
diff --git a/src/main/java/org/zwobble/couscous/ast/visitors/StatementNodeVisitor.java b/src/main/java/org/zwobble/couscous/ast/visitors/StatementNodeVisitor.java
new file mode 100644
index 0000000..6cec404
--- /dev/null
+++ b/src/main/java/org/zwobble/couscous/ast/visitors/StatementNodeVisitor.java
@@ -0,0 +1,9 @@
+package org.zwobble.couscous.ast.visitors;
+
+import org.zwobble.couscous.ast.ExpressionStatementNode;
+import org.zwobble.couscous.ast.ReturnNode;
+
+public interface StatementNodeVisitor<T> {
+ T visit(ReturnNode returnNode);
+ T visit(ExpressionStatementNode expressionStatement);
+}
diff --git a/src/main/java/org/zwobble/couscous/interpreter/Interpreter.java b/src/main/java/org/zwobble/couscous/interpreter/Interpreter.java
new file mode 100644
index 0000000..49c651b
--- /dev/null
+++ b/src/main/java/org/zwobble/couscous/interpreter/Interpreter.java
@@ -0,0 +1,110 @@
+package org.zwobble.couscous.interpreter;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.IntStream;
+
+import org.zwobble.couscous.Project;
+import org.zwobble.couscous.ast.Assignment;
+import org.zwobble.couscous.ast.ExpressionNode;
+import org.zwobble.couscous.ast.ExpressionStatementNode;
+import org.zwobble.couscous.ast.LiteralNode;
+import org.zwobble.couscous.ast.MethodNode;
+import org.zwobble.couscous.ast.ReturnNode;
+import org.zwobble.couscous.ast.VariableReferenceNode;
+import org.zwobble.couscous.ast.visitors.ExpressionNodeVisitor;
+import org.zwobble.couscous.ast.visitors.StatementNodeVisitor;
+import org.zwobble.couscous.values.InterpreterValue;
+
+import static com.google.common.collect.Iterables.find;
+import static java.util.stream.Collectors.toMap;
+import static org.zwobble.couscous.values.UnitValue.UNIT;
+
+import lombok.val;
+
+public class Interpreter {
+ private Project project;
+
+ public Interpreter(Project project) {
+ this.project = project;
+ }
+
+ public InterpreterValue run(String className, String methodName, List<InterpreterValue> arguments) {
+ val clazz = project.findClass(className);
+ val method = find(clazz.getMethods(),
+ method -> method.getName().equals(methodName));
+ val stackFrame = buildStackFrame(method, arguments);
+
+ for (val statement : method.getBody()) {
+ val result = statement.accept(new Executor(new Evaluator(stackFrame)));
+ if (result.isPresent()) {
+ return result.get();
+ }
+ }
+ return UNIT;
+ }
+
+ private Map<Integer, InterpreterValue> buildStackFrame(
+ final MethodNode method,
+ List<InterpreterValue> arguments) {
+
+ if (method.getArguments().size() != arguments.size()) {
+ throw new WrongNumberOfArguments(method.getArguments().size(), arguments.size());
+ }
+
+ return IntStream.range(0, method.getArguments().size())
+ .boxed()
+ .collect(toMap(
+ index -> method.getArguments().get(index).getId(),
+ index -> arguments.get(index)));
+ }
+
+ private class Executor implements StatementNodeVisitor<Optional<InterpreterValue>> {
+ private Evaluator evaluator;
+
+ public Executor(Evaluator evaluator) {
+ this.evaluator = evaluator;
+ }
+
+ @Override
+ public Optional<InterpreterValue> visit(ReturnNode returnNode) {
+ return Optional.of(evaluator.eval(returnNode.getValue()));
+ }
+
+ @Override
+ public Optional<InterpreterValue> visit(ExpressionStatementNode expressionStatement) {
+ evaluator.eval(expressionStatement.getExpression());
+ return Optional.empty();
+ }
+ }
+
+ private class Evaluator implements ExpressionNodeVisitor<InterpreterValue> {
+ private final Map<Integer, InterpreterValue> stackFrame;
+
+ public Evaluator(Map<Integer, InterpreterValue> stackFrame) {
+ this.stackFrame = stackFrame;
+ }
+
+ public InterpreterValue eval(ExpressionNode expression) {
+ return expression.accept(this);
+ }
+
+ @Override
+ public InterpreterValue visit(LiteralNode literal) {
+ return literal.getValue();
+ }
+
+ @Override
+ public InterpreterValue visit(VariableReferenceNode variableReference) {
+ return stackFrame.get(variableReference.getReferentId());
+ }
+
+ @Override
+ public InterpreterValue visit(Assignment assignment) {
+ val value = eval(assignment.getValue());
+ stackFrame.put(assignment.getTarget().getReferentId(), value);
+ return value;
+ }
+ }
+}
diff --git a/src/main/java/org/zwobble/couscous/interpreter/InterpreterException.java b/src/main/java/org/zwobble/couscous/interpreter/InterpreterException.java
new file mode 100644
index 0000000..d6d7020
--- /dev/null
+++ b/src/main/java/org/zwobble/couscous/interpreter/InterpreterException.java
@@ -0,0 +1,6 @@
+package org.zwobble.couscous.interpreter;
+
+public class InterpreterException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+}
diff --git a/src/main/java/org/zwobble/couscous/interpreter/WrongNumberOfArguments.java b/src/main/java/org/zwobble/couscous/interpreter/WrongNumberOfArguments.java
new file mode 100644
index 0000000..06d8079
--- /dev/null
+++ b/src/main/java/org/zwobble/couscous/interpreter/WrongNumberOfArguments.java
@@ -0,0 +1,13 @@
+package org.zwobble.couscous.interpreter;
+
+import lombok.EqualsAndHashCode;
+import lombok.Value;
+
+@Value
+@EqualsAndHashCode(callSuper=false)
+public class WrongNumberOfArguments extends InterpreterException {
+ private static final long serialVersionUID = 1L;
+
+ int expected;
+ int actual;
+}
diff --git a/src/main/java/org/zwobble/couscous/values/IntegerValue.java b/src/main/java/org/zwobble/couscous/values/IntegerValue.java
new file mode 100644
index 0000000..062d137
--- /dev/null
+++ b/src/main/java/org/zwobble/couscous/values/IntegerValue.java
@@ -0,0 +1,8 @@
+package org.zwobble.couscous.values;
+
+import lombok.Value;
+
+@Value
+public class IntegerValue implements InterpreterValue {
+ int value;
+}
diff --git a/src/main/java/org/zwobble/couscous/values/InterpreterValue.java b/src/main/java/org/zwobble/couscous/values/InterpreterValue.java
new file mode 100644
index 0000000..64b0e91
--- /dev/null
+++ b/src/main/java/org/zwobble/couscous/values/InterpreterValue.java
@@ -0,0 +1,5 @@
+package org.zwobble.couscous.values;
+
+public interface InterpreterValue {
+
+}
diff --git a/src/main/java/org/zwobble/couscous/values/StringValue.java b/src/main/java/org/zwobble/couscous/values/StringValue.java
new file mode 100644
index 0000000..1478d41
--- /dev/null
+++ b/src/main/java/org/zwobble/couscous/values/StringValue.java
@@ -0,0 +1,8 @@
+package org.zwobble.couscous.values;
+
+import lombok.Value;
+
+@Value
+public class StringValue implements InterpreterValue {
+ String value;
+}
diff --git a/src/main/java/org/zwobble/couscous/values/UnitValue.java b/src/main/java/org/zwobble/couscous/values/UnitValue.java
new file mode 100644
index 0000000..1cf9dc7
--- /dev/null
+++ b/src/main/java/org/zwobble/couscous/values/UnitValue.java
@@ -0,0 +1,11 @@
+package org.zwobble.couscous.values;
+
+import lombok.ToString;
+
+@ToString
+public class UnitValue implements InterpreterValue {
+ public static final UnitValue UNIT = new UnitValue();
+
+ private UnitValue() {
+ }
+}
diff --git a/src/test/java/org/zwobble/couscous/tests/interpreter/InterpreterTests.java b/src/test/java/org/zwobble/couscous/tests/interpreter/InterpreterTests.java
new file mode 100644
index 0000000..d3bc21c
--- /dev/null
+++ b/src/test/java/org/zwobble/couscous/tests/interpreter/InterpreterTests.java
@@ -0,0 +1,117 @@
+package org.zwobble.couscous.tests.interpreter;
+
+import java.util.Map;
+
+import org.junit.Test;
+import org.zwobble.couscous.Project;
+import org.zwobble.couscous.ast.Assignment;
+import org.zwobble.couscous.ast.ClassNode;
+import org.zwobble.couscous.ast.ExpressionStatementNode;
+import org.zwobble.couscous.ast.FormalArgumentNode;
+import org.zwobble.couscous.ast.LiteralNode;
+import org.zwobble.couscous.ast.MethodNode;
+import org.zwobble.couscous.ast.ReturnNode;
+import org.zwobble.couscous.interpreter.Interpreter;
+import org.zwobble.couscous.interpreter.WrongNumberOfArguments;
+import org.zwobble.couscous.values.InterpreterValue;
+import org.zwobble.couscous.values.StringValue;
+
+import com.google.common.collect.ImmutableMap;
+
+import static java.util.Arrays.asList;
+import static org.junit.Assert.assertEquals;
+import static org.zwobble.couscous.ast.MethodNode.staticMethod;
+import static org.zwobble.couscous.ast.VariableReferenceNode.reference;
+import static org.zwobble.couscous.tests.util.ExtraAsserts.assertThrows;
+import static org.zwobble.couscous.values.UnitValue.UNIT;
+
+import lombok.val;
+
+public class InterpreterTests {
+ @Test
+ public void canReturnLiteralValue() {
+ val method = staticMethod("hello")
+ .statement(new ReturnNode(LiteralNode.of("hello, world!")));
+ val result = runMethod(method);
+
+ assertEquals(new StringValue("hello, world!"), result);
+ }
+
+ @Test
+ public void canPassValueToMethod() {
+ val arg = new FormalArgumentNode(42, "x");
+ val method = staticMethod("hello")
+ .argument(arg)
+ .statement(new ReturnNode(reference(arg)));
+ val result = runMethod(method, new StringValue("hello, world!"));
+
+ assertEquals(new StringValue("hello, world!"), result);
+ }
+
+ @Test
+ public void errorIfWrongNumberOfArgumentsArePassed() {
+ val method = staticMethod("hello");
+
+ val exception = assertThrows(WrongNumberOfArguments.class,
+ () -> runMethod(method, new StringValue("hello, world!")));
+
+ assertEquals(new WrongNumberOfArguments(0, 1), exception);
+ }
+
+ @Test
+ public void canReassignValueToArgument() {
+ val arg = new FormalArgumentNode(42, "x");
+ val method = staticMethod("hello")
+ .argument(arg)
+ .statement(new ExpressionStatementNode(new Assignment(reference(arg), LiteralNode.of("[updated value]"))))
+ .statement(new ReturnNode(reference(arg)));
+ val result = runMethod(method, new StringValue("[initial value]"));
+
+ assertEquals(new StringValue("[updated value]"), result);
+ }
+
+ @Test
+ public void valueOfAssignmentExpressionIsNewValue() {
+ val arg = new FormalArgumentNode(42, "x");
+ val method = staticMethod("hello")
+ .argument(arg)
+ .statement(new ReturnNode(new Assignment(reference(arg), LiteralNode.of("[updated value]"))));
+ val result = runMethod(method, new StringValue("[initial value]"));
+
+ assertEquals(new StringValue("[updated value]"), result);
+ }
+
+ @Test
+ public void methodWithNoStatementsReturnsUnit() {
+ val method = staticMethod("hello");
+ val result = runMethod(method);
+
+ assertEquals(UNIT, result);
+ }
+
+ private InterpreterValue runMethod(MethodNode.MethodNodeBuilder methodBuilder, InterpreterValue... arguments) {
+ val method = methodBuilder.build();
+ val classNode = ClassNode.builder()
+ .method(method)
+ .build();
+ val className = "com.example.Program";
+ val interpreter = new Interpreter(new MapBackedProject(ImmutableMap.of(
+ className, classNode)));
+
+ return interpreter.run(className, method.getName(), asList(arguments));
+ }
+
+ private static class MapBackedProject implements Project {
+ private Map<String, ClassNode> classes;
+
+ public MapBackedProject(Map<String, ClassNode> classes) {
+ this.classes = classes;
+ }
+
+ @Override
+ public ClassNode findClass(String name) {
+ return classes.get(name);
+ }
+
+ }
+}
diff --git a/src/test/java/org/zwobble/couscous/tests/util/ExtraAsserts.java b/src/test/java/org/zwobble/couscous/tests/util/ExtraAsserts.java
new file mode 100644
index 0000000..8b1c16c
--- /dev/null
+++ b/src/test/java/org/zwobble/couscous/tests/util/ExtraAsserts.java
@@ -0,0 +1,21 @@
+package org.zwobble.couscous.tests.util;
+
+public class ExtraAsserts {
+ public interface Action {
+ void run();
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T extends Throwable> T assertThrows(Class<T> exceptionType, Action action) {
+ try {
+ action.run();
+ throw new AssertionError("Expected exception");
+ } catch (Exception exception) {
+ if (exceptionType.isInstance(exception)) {
+ return (T)exception;
+ } else {
+ throw exception;
+ }
+ }
+ }
+}