summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarlo Zancanaro <carlo@zancanaro.id.au>2015-06-05 14:51:03 +1000
committerCarlo Zancanaro <carlo@zancanaro.id.au>2015-06-05 14:51:03 +1000
commit20b1226b4eb10e85497862bd73fe9e9a2f05191d (patch)
treecfc2cf01032515af4adba2a659f3da9734f5bb48
parent821f5a2c711d748a95d8f5d54266069c5378b556 (diff)
First cut of a stateful testing framework (it's pretty hacky at the moment, but all the pieces are there)
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/statem/Command.java23
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/statem/CommandList.java85
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/statem/CommandListGenerator.java50
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/statem/CommandResult.java31
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/statem/CommandValue.java60
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/statem/GeneratedCommand.java56
-rw-r--r--src/test/java/au/id/zancanaro/javacheck/statem/QueueTest.java88
7 files changed, 393 insertions, 0 deletions
diff --git a/src/main/java/au/id/zancanaro/javacheck/statem/Command.java b/src/main/java/au/id/zancanaro/javacheck/statem/Command.java
new file mode 100644
index 0000000..a741f0a
--- /dev/null
+++ b/src/main/java/au/id/zancanaro/javacheck/statem/Command.java
@@ -0,0 +1,23 @@
+package au.id.zancanaro.javacheck.statem;
+
+import au.id.zancanaro.javacheck.Generator;
+
+public abstract class Command<State,Args,Result> {
+ public Generator<Args> argsGenerator(State state) {
+ return Generator.pure(null);
+ }
+
+ public boolean preCondition(State state, Args args) {
+ return true;
+ }
+
+ public abstract Result runCommand(Args args);
+
+ public State nextState(State state, Args args, CommandValue<Result> result) {
+ return state;
+ }
+
+ public boolean postCondition(State oldState, State newState, Args args, Result result) {
+ return true;
+ }
+}
diff --git a/src/main/java/au/id/zancanaro/javacheck/statem/CommandList.java b/src/main/java/au/id/zancanaro/javacheck/statem/CommandList.java
new file mode 100644
index 0000000..9e8948a
--- /dev/null
+++ b/src/main/java/au/id/zancanaro/javacheck/statem/CommandList.java
@@ -0,0 +1,85 @@
+package au.id.zancanaro.javacheck.statem;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class CommandList<State> {
+ private final List<GeneratedCommand<State, ?, ?>> commands;
+
+ public CommandList(List<GeneratedCommand<State, ?, ?>> commands) {
+ this.commands = new ArrayList<>(commands);
+ }
+
+ public CommandResult<State> run(State initialState) {
+ Map<Integer, Object> values = new HashMap<>();
+ CommandResult<State> result = CommandResult.success(initialState);
+ for (GeneratedCommand<State, ?, ?> generated : commands) {
+ result = runRealCommand(generated, result.getState(), values);
+ if (result.isFailed()) {
+ break;
+ }
+ }
+ return result;
+ }
+
+ private static <State, Args, Result> CommandResult<State> runRealCommand(
+ GeneratedCommand<State, Args, Result> generated,
+ State state,
+ Map<Integer, Object> values) {
+ int id = generated.getId();
+ Command<State, Args, Result> command = generated.getCommand();
+ Args args = generated.getArgs();
+ try {
+ if (!command.preCondition(state, args)) {
+ return CommandResult.fail(state, new Error("Precondition failed"));
+ }
+ Result result = command.runCommand(args);
+ values.put(id, result);
+ State oldState = state;
+ state = command.nextState(state, args, new CommandValue.ConcreteValue<>(id, values));
+ if (!command.postCondition(oldState, state, args, result)) {
+ return CommandResult.fail(state, new Error("Postcondition failed"));
+ }
+ return CommandResult.success(state);
+ } catch (Throwable ex) {
+ return CommandResult.fail(state, ex);
+ }
+ }
+
+ private static <State, Args, Result> CommandResult<State> runAbstractCommand(
+ GeneratedCommand<State, Args, Result> generated,
+ State state) {
+ int id = generated.getId();
+ Command<State, Args, Result> command = generated.getCommand();
+ Args args = generated.getArgs();
+ try {
+ if (!command.preCondition(state, args)) {
+ return CommandResult.fail(state, new Error("Precondition failed"));
+ }
+ state = command.nextState(state, args, new CommandValue.AbstractValue<>(id));
+ return CommandResult.success(state);
+ } catch (Throwable ex) {
+ return CommandResult.fail(state, ex);
+ }
+ }
+
+ public boolean isValid() {
+ CommandResult<State> result = CommandResult.success(null);
+ for (GeneratedCommand<State, ?, ?> generated : commands) {
+ result = runAbstractCommand(generated, result.getState());
+ if (result.isFailed()) {
+ break;
+ }
+ }
+ return !result.isFailed();
+ }
+
+ @Override
+ public String toString() {
+ return "CommandList{" +
+ "commands=" + commands +
+ '}';
+ }
+}
diff --git a/src/main/java/au/id/zancanaro/javacheck/statem/CommandListGenerator.java b/src/main/java/au/id/zancanaro/javacheck/statem/CommandListGenerator.java
new file mode 100644
index 0000000..a0df66f
--- /dev/null
+++ b/src/main/java/au/id/zancanaro/javacheck/statem/CommandListGenerator.java
@@ -0,0 +1,50 @@
+package au.id.zancanaro.javacheck.statem;
+
+import au.id.zancanaro.javacheck.Generator;
+import au.id.zancanaro.javacheck.ShrinkTree;
+
+import java.util.Random;
+import java.util.function.Function;
+
+import static au.id.zancanaro.javacheck.Generator.pure;
+import static au.id.zancanaro.javacheck.Generators.noShrink;
+
+public class CommandListGenerator<State> implements Generator<CommandList<State>> {
+ private final Function<State, Generator<Command<State, ?, ?>>> generateCommand;
+
+ public CommandListGenerator(Function<State, Generator<Command<State, ?, ?>>> generateCommand) {
+ this.generateCommand = generateCommand;
+ }
+
+ public Generator<GeneratedCommand<State, ?, ?>> commandGenerator(int id, State state) {
+ return noShrink(generateCommand.apply(state))
+ .flatMap(command -> generateSingleCommand(id, command, state));
+ }
+
+ public <Args, Result> Generator<GeneratedCommand<State, ?, ?>> generateSingleCommand(int id, Command<State, Args, Result> command, State state) {
+ return command.argsGenerator(state).flatMap(generatedArgs ->
+ command.preCondition(state, generatedArgs) ?
+ pure(new GeneratedCommand<>(id, command, generatedArgs)) :
+ commandGenerator(id, state));
+ }
+
+ public <Args, Result> State nextState(int id, GeneratedCommand<State, Args, Result> generatedCommand, State state) {
+ return generatedCommand.getCommand().nextState(state, generatedCommand.getArgs(), new CommandValue.AbstractValue<>(id));
+ }
+
+ @Override
+ public ShrinkTree<CommandList<State>> generate(Random random, int size) {
+ int count = random.nextInt(size);
+ @SuppressWarnings("unchecked")
+ ShrinkTree<GeneratedCommand<State, ?, ?>>[] commandTrees = (ShrinkTree<GeneratedCommand<State, ?, ?>>[]) new ShrinkTree[count];
+ State state = null;
+ for (int i = 0; i < count; ++i) {
+ commandTrees[i] = commandGenerator(i, state).generate(random, size);
+ GeneratedCommand<State, ?, ?> generatedCommand = commandTrees[i].getValue();
+ state = nextState(i, generatedCommand, state);
+ }
+ return ShrinkTree.combine(commandTrees, ShrinkTree::removeAndPromoteChildren)
+ .map(list -> new CommandList<>(list))
+ .filter(CommandList::isValid);
+ }
+}
diff --git a/src/main/java/au/id/zancanaro/javacheck/statem/CommandResult.java b/src/main/java/au/id/zancanaro/javacheck/statem/CommandResult.java
new file mode 100644
index 0000000..dc5b085
--- /dev/null
+++ b/src/main/java/au/id/zancanaro/javacheck/statem/CommandResult.java
@@ -0,0 +1,31 @@
+package au.id.zancanaro.javacheck.statem;
+
+public class CommandResult<State> {
+ private final State state;
+ private final Throwable thrown;
+
+ private CommandResult(State state, Throwable thrown) {
+ this.state = state;
+ this.thrown = thrown;
+ }
+
+ public State getState() {
+ return state;
+ }
+
+ public boolean isFailed() {
+ return thrown != null;
+ }
+
+ public Throwable getThrown() {
+ return thrown;
+ }
+
+ public static <State> CommandResult<State> success(State state) {
+ return new CommandResult<>(state, null);
+ }
+
+ public static <State> CommandResult<State> fail(State state, Throwable ex) {
+ return new CommandResult<>(state, ex);
+ }
+}
diff --git a/src/main/java/au/id/zancanaro/javacheck/statem/CommandValue.java b/src/main/java/au/id/zancanaro/javacheck/statem/CommandValue.java
new file mode 100644
index 0000000..0a1fb61
--- /dev/null
+++ b/src/main/java/au/id/zancanaro/javacheck/statem/CommandValue.java
@@ -0,0 +1,60 @@
+package au.id.zancanaro.javacheck.statem;
+
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+public abstract class CommandValue<T> {
+ private final int id;
+
+ public CommandValue(int id) {
+ this.id = id;
+ }
+
+ public abstract boolean isAbstract();
+
+ public abstract T get();
+
+ public int getId() {
+ return id;
+ }
+
+ static class AbstractValue<T> extends CommandValue<T> {
+ public AbstractValue(int id) {
+ super(id);
+ }
+
+ @Override
+ public boolean isAbstract() {
+ return true;
+ }
+
+ @Override
+ public T get() {
+ throw new NoSuchElementException("Abstract values cannot be supplied");
+ }
+ }
+
+ static class ConcreteValue<T> extends CommandValue<T> {
+ private final Map<Integer, Object> values;
+
+ public ConcreteValue(int id, Map<Integer, Object> values) {
+ super(id);
+ this.values = values;
+ }
+
+ @Override
+ public boolean isAbstract() {
+ return true;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public T get() {
+ if (values.containsKey(getId())) {
+ return (T) values.get(getId());
+ } else {
+ throw new NoSuchElementException("Concrete values cannot be supplied prior to being calculated");
+ }
+ }
+ }
+}
diff --git a/src/main/java/au/id/zancanaro/javacheck/statem/GeneratedCommand.java b/src/main/java/au/id/zancanaro/javacheck/statem/GeneratedCommand.java
new file mode 100644
index 0000000..5e576b4
--- /dev/null
+++ b/src/main/java/au/id/zancanaro/javacheck/statem/GeneratedCommand.java
@@ -0,0 +1,56 @@
+package au.id.zancanaro.javacheck.statem;
+
+public class GeneratedCommand<State, Args, Result> {
+ private final int id;
+ private final Command<State, Args, Result> command;
+ private final Args args;
+
+ public GeneratedCommand(int id, Command<State, Args, Result> command, Args args) {
+ this.id = id;
+ this.command = command;
+ this.args = args;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public Command<State, Args, Result> getCommand() {
+ return command;
+ }
+
+ public Args getArgs() {
+ return args;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ GeneratedCommand that = (GeneratedCommand) o;
+
+ if (args != null ? !args.equals(that.args) : that.args != null)
+ return false;
+ if (command != null ? !command.equals(that.command) : that.command != null)
+ return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = command != null ? command.hashCode() : 0;
+ result = 31 * result + (args != null ? args.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "GeneratedCommand{" +
+ "id=" + id +
+ ", command=" + command +
+ ", args=" + args +
+ '}';
+ }
+}
diff --git a/src/test/java/au/id/zancanaro/javacheck/statem/QueueTest.java b/src/test/java/au/id/zancanaro/javacheck/statem/QueueTest.java
new file mode 100644
index 0000000..4cd6345
--- /dev/null
+++ b/src/test/java/au/id/zancanaro/javacheck/statem/QueueTest.java
@@ -0,0 +1,88 @@
+package au.id.zancanaro.javacheck.statem;
+
+import au.id.zancanaro.javacheck.Generator;
+import au.id.zancanaro.javacheck.annotations.DataSource;
+import au.id.zancanaro.javacheck.annotations.Property;
+import au.id.zancanaro.javacheck.junit.Properties;
+import org.junit.runner.RunWith;
+
+import java.util.*;
+
+import static au.id.zancanaro.javacheck.Generator.pure;
+import static au.id.zancanaro.javacheck.Generators.integer;
+import static au.id.zancanaro.javacheck.Generators.oneOf;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(Properties.class)
+public class QueueTest {
+ private static final Queue<Integer> queue = new LinkedList<>();
+
+ @DataSource
+ public static Generator<CommandList<List<Integer>>> commandGenerator =
+ new CommandListGenerator<>(state -> oneOf(
+ pure(new QueuePushCommand()),
+ pure(new QueuePopCommand())
+ ));
+
+ @Property
+ public void test(CommandList<List<Integer>> commands) {
+ queue.clear();
+ CommandResult<List<Integer>> result = commands.run(null);
+ assertFalse(result.isFailed());
+ }
+
+ private static class QueuePushCommand extends Command<List<Integer>, Integer, Void> {
+ @Override
+ public Generator<Integer> argsGenerator(List<Integer> integers) {
+ return integer();
+ }
+
+ @Override
+ public Void runCommand(Integer integer) {
+ queue.offer(integer);
+ return null;
+ }
+
+ @Override
+ public List<Integer> nextState(List<Integer> integers, Integer integer, CommandValue<Void> result) {
+ List<Integer> nextState = new ArrayList<>(integers == null ? new ArrayList<>() : integers);
+ nextState.add(integer);
+ return nextState;
+ }
+
+ @Override
+ public String toString() {
+ return "push";
+ }
+ }
+
+ private static class QueuePopCommand extends Command<List<Integer>, Void, Integer> {
+ @Override
+ public boolean preCondition(List<Integer> integers, Void aVoid) {
+ return integers != null && !integers.isEmpty();
+ }
+
+ @Override
+ public Integer runCommand(Void aVoid) {
+ return queue.poll();
+ }
+
+ @Override
+ public boolean postCondition(List<Integer> oldState, List<Integer> newState, Void aVoid, Integer integer) {
+ return Objects.equals(integer, oldState.get(0));
+ }
+
+ @Override
+ public List<Integer> nextState(List<Integer> integers, Void aVoid, CommandValue<Integer> result) {
+ List<Integer> nextState = new ArrayList<>(integers == null ? new ArrayList<>() : integers);
+ nextState.remove(0);
+ return nextState;
+ }
+
+ @Override
+ public String toString() {
+ return "pop";
+ }
+ }
+}