diff options
author | Carlo Zancanaro <carlo@zancanaro.id.au> | 2015-06-05 14:51:03 +1000 |
---|---|---|
committer | Carlo Zancanaro <carlo@zancanaro.id.au> | 2015-06-05 14:51:03 +1000 |
commit | 20b1226b4eb10e85497862bd73fe9e9a2f05191d (patch) | |
tree | cfc2cf01032515af4adba2a659f3da9734f5bb48 | |
parent | 821f5a2c711d748a95d8f5d54266069c5378b556 (diff) |
First cut of a stateful testing framework (it's pretty hacky at the moment, but all the pieces are there)
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"; + } + } +} |