You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by dc...@apache.org on 2024/09/04 17:59:44 UTC
(cassandra-accord) branch trunk updated: Create a fuzz test that randomizes topology changes, cluster actions, and CQL operations (#119)
This is an automated email from the ASF dual-hosted git repository.
dcapwell pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra-accord.git
The following commit(s) were added to refs/heads/trunk by this push:
new a171322f Create a fuzz test that randomizes topology changes, cluster actions, and CQL operations (#119)
a171322f is described below
commit a171322f417c117733ca5b514d03a5202b1ac202
Author: dcapwell <dc...@apache.org>
AuthorDate: Wed Sep 4 10:59:39 2024 -0700
Create a fuzz test that randomizes topology changes, cluster actions, and CQL operations (#119)
patch by David Capwell; reviewed by Alex Petrov for CASSANDRA-19847
---
accord-core/src/test/java/accord/utils/Gens.java | 87 +++-
.../src/test/java/accord/utils/GensTest.java | 90 ++++
.../src/test/java/accord/utils/Property.java | 466 +++++++++++++++++++--
.../src/test/java/accord/utils/SeedProvider.java | 51 +++
.../test/java/accord/utils/async/TimeoutUtils.java | 65 +++
5 files changed, 723 insertions(+), 36 deletions(-)
diff --git a/accord-core/src/test/java/accord/utils/Gens.java b/accord-core/src/test/java/accord/utils/Gens.java
index bf478c76..6cda07c6 100644
--- a/accord-core/src/test/java/accord/utils/Gens.java
+++ b/accord-core/src/test/java/accord/utils/Gens.java
@@ -41,6 +41,8 @@ import java.util.stream.IntStream;
import java.util.stream.LongStream;
import java.util.stream.Stream;
+import com.google.common.collect.Iterables;
+
import accord.utils.random.Picker;
public class Gens {
@@ -122,6 +124,8 @@ public class Gens {
// this method relies on the map having some order and will reject any map that doesn't define a deterministic order
if (!(values instanceof EnumMap || values instanceof LinkedHashMap))
throw new IllegalArgumentException("pick(Map) requires a map with deterministic iteration; given " + values.getClass());
+ if (values.size() == 1)
+ return constant(Objects.requireNonNull(Iterables.getFirst(values.keySet(), null)));
double totalWeight = values.values().stream().mapToDouble(Integer::intValue).sum();
List<Weight<T>> list = new ArrayList<>(values.size());
Iterator<Map.Entry<T, Integer>> it = values.entrySet().iterator();
@@ -262,6 +266,56 @@ public class Gens {
return i;
}
+ public static Gen<Gen.IntGen> mixedDistribution(int minInclusive, int maxExclusive, int numBuckets)
+ {
+ int domainSize = (maxExclusive - minInclusive);
+ if (domainSize < 0)
+ throw new IllegalArgumentException("Range is too large; min=" + minInclusive + ", max=" + maxExclusive);
+ if (numBuckets <= 0 || numBuckets > domainSize)
+ throw new IllegalArgumentException("Num buckets must be between 1 and " + domainSize + "; given " + numBuckets);
+ int[] bucket, indexes;
+ bucket = new int[numBuckets];
+ int delta = domainSize / numBuckets;
+ for (int i = 0; i < numBuckets; i++)
+ bucket[i] = minInclusive + i * delta;
+ indexes = IntStream.range(0, bucket.length).toArray();
+ Gen<Gen.IntGen> indexDistro = mixedDistribution(indexes);
+ return rs -> {
+ Gen.IntGen indexGen = indexDistro.next(rs);
+ switch (rs.nextInt(0, 2))
+ {
+ case 0: // uniform
+ {
+ return r -> {
+ int idx = indexGen.next(rs);
+ int start = bucket[idx];
+ int end = idx == bucket.length - 1 ? maxExclusive : bucket[idx + 1];
+ return r.nextInt(start, end);
+ };
+ }
+ case 1: // median biased
+ {
+ int medians[] = new int[bucket.length];
+ for (int i = 0; i < medians.length; i++)
+ {
+ int start = bucket[i];
+ int end = i == bucket.length - 1 ? maxExclusive : bucket[i + 1];
+ medians[i] = rs.nextInt(start, end);
+ }
+ return r -> {
+ int idx = indexGen.next(rs);
+ int start = bucket[idx];
+ int end = idx == bucket.length - 1 ? maxExclusive : bucket[idx + 1];
+ int median = medians[idx];
+ return r.nextBiasedInt(start, median, end);
+ };
+ }
+ default:
+ throw new AssertionError();
+ }
+ };
+ }
+
public static Gen<Gen.IntGen> mixedDistribution(int minInclusive, int maxExclusive)
{
int domainSize = (maxExclusive - minInclusive + 1);
@@ -418,6 +472,32 @@ public class Gens {
};
}
+ public static <T> Gen<Gen.IntGen> mixedDistribution(int[] list)
+ {
+ return rs -> {
+ switch (rs.nextInt(0, 4))
+ {
+ case 0: // uniform
+ return r -> list[rs.nextInt(0, list.length)];
+ case 1: // median biased
+ int median = rs.nextInt(0, list.length);
+ return r -> list[r.nextBiasedInt(0, median, list.length)];
+ case 2: // zipf
+ int[] array = list;
+ if (rs.nextBoolean())
+ {
+ array = Arrays.copyOf(array, array.length);
+ Utils.reverse(array);
+ }
+ return pickZipf(array);
+ case 3: // random weight
+ return randomWeights(list).next(rs);
+ default:
+ throw new AssertionError();
+ }
+ };
+ }
+
public static Gen<char[]> charArray(Gen.IntGen sizes, char[] domain)
{
return charArray(sizes, domain, (a, b) -> true);
@@ -599,6 +679,11 @@ public class Gens {
{
return Gens.mixedDistribution(minInclusive, maxExclusive);
}
+
+ public Gen<Gen.IntGen> mixedDistribution(int minInclusive, int maxExclusive, int numBuckets)
+ {
+ return Gens.mixedDistribution(minInclusive, maxExclusive, numBuckets);
+ }
}
public static class LongDSL {
@@ -870,7 +955,7 @@ public class Gens {
}
}
- private interface Reset {
+ protected interface Reset {
static void tryReset(Object o)
{
if (o instanceof Reset)
diff --git a/accord-core/src/test/java/accord/utils/GensTest.java b/accord-core/src/test/java/accord/utils/GensTest.java
new file mode 100644
index 00000000..5ba5d17e
--- /dev/null
+++ b/accord-core/src/test/java/accord/utils/GensTest.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package accord.utils;
+
+import org.junit.jupiter.api.Test;
+
+import org.assertj.core.api.Assertions;
+import org.mockito.Mockito;
+import org.mockito.stubbing.Answer;
+
+class GensTest
+{
+ private static final Answer REJECT_ALL = ignore -> {
+ throw new UnsupportedOperationException(ignore.toString());
+ };
+
+ @Test
+ void intMixedDistributionWithBucketsOutOfBounds()
+ {
+ Assertions.assertThatThrownBy(() -> Gens.mixedDistribution(0, 100, 101))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Num buckets must be between 1 and 100; given 101");
+ }
+
+ @Test
+ void intMixedDistributionWithBucketsUniform()
+ {
+ Gen<Gen.IntGen> distroGren = Gens.mixedDistribution(0, 100, 10);
+ RandomSource rs = Mockito.mock(RandomSource.class, REJECT_ALL);
+ Mockito.doReturn(0).when(rs).nextInt(0, 4); // uniform distro for bucket selection
+ Mockito.doReturn(0).when(rs).nextInt(0, 2); // uniform distro for within the bucket
+
+ Gen.IntGen gen = distroGren.next(rs);
+
+ // to avoid conflicts, need to have i=0 as its own thing
+ Mockito.reset(rs);
+ Mockito.doReturn(0, 42).when(rs).nextInt(0, 10);
+ Assertions.assertThat(gen.next(rs)).isEqualTo(42);
+
+ for (int i = 1; i < 10; i++)
+ {
+ int start = i * 10;
+ int end = start + 10;
+ Mockito.reset(rs);
+ Mockito.doReturn(i).when(rs).nextInt(0, 10);
+ Mockito.doReturn(42).when(rs).nextInt(start, end); // this is the value returned
+ Assertions.assertThat(gen.next(rs)).isEqualTo(42);
+ }
+ }
+
+ @Test
+ void intMixedDistributionWithBucketsMedian()
+ {
+ Gen<Gen.IntGen> distroGren = Gens.mixedDistribution(0, 100, 10);
+ RandomSource rs = Mockito.mock(RandomSource.class, REJECT_ALL);
+ Mockito.doReturn(0).when(rs).nextInt(0, 4); // uniform distro for bucket selection
+ Mockito.doReturn(1).when(rs).nextInt(0, 2); // median distro for within the bucket
+ // populate the median buckets
+ for (int start = 0; start < 100; start += 10)
+ Mockito.doReturn(start).when(rs).nextInt(start, start + 10);
+
+ Gen.IntGen gen = distroGren.next(rs);
+
+ for (int i = 1; i < 10; i++)
+ {
+ int start = i * 10;
+ int end = start + 10;
+ Mockito.reset(rs);
+ Mockito.doReturn(i).when(rs).nextInt(0, 10);
+ Mockito.doReturn(42).when(rs).nextBiasedInt(start, start, end); // this is the value returned
+ Assertions.assertThat(gen.next(rs)).isEqualTo(42);
+ }
+ }
+}
\ No newline at end of file
diff --git a/accord-core/src/test/java/accord/utils/Property.java b/accord-core/src/test/java/accord/utils/Property.java
index 50eba0c1..e45642c1 100644
--- a/accord-core/src/test/java/accord/utils/Property.java
+++ b/accord-core/src/test/java/accord/utils/Property.java
@@ -18,20 +18,24 @@
package accord.utils;
-import accord.utils.async.AsyncChains;
-import accord.utils.async.AsyncResult;
-import accord.utils.async.AsyncResults;
+import accord.utils.async.TimeoutUtils;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ThreadLocalRandom;
-import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
@@ -39,7 +43,7 @@ public class Property
{
public static abstract class Common<T extends Common<T>>
{
- protected long seed = ThreadLocalRandom.current().nextLong();
+ protected long seed = SeedProvider.instance.nextSeed();
protected int examples = 1000;
protected boolean pure = true;
@@ -85,24 +89,9 @@ public class Property
protected void checkWithTimeout(Runnable fn)
{
- AsyncResult.Settable<?> promise = AsyncResults.settable();
- Thread t = new Thread(() -> {
- try
- {
- fn.run();
- promise.setSuccess(null);
- }
- catch (Throwable e)
- {
- promise.setFailure(e);
- }
- });
- t.setName("property with timeout");
- t.setDaemon(true);
try
{
- t.start();
- AsyncChains.getBlocking(promise, timeout.toNanos(), TimeUnit.NANOSECONDS);
+ TimeoutUtils.runBlocking(timeout, "property with timeout", fn::run);
}
catch (ExecutionException e)
{
@@ -110,12 +99,10 @@ public class Property
}
catch (InterruptedException e)
{
- t.interrupt();
throw new PropertyError(propertyError(this, e));
}
catch (TimeoutException e)
{
- t.interrupt();
TimeoutException override = new TimeoutException("property test did not complete within " + this.timeout);
override.setStackTrace(new StackTraceElement[0]);
throw new PropertyError(propertyError(this, override));
@@ -222,10 +209,12 @@ public class Property
StringBuilder sb = propertyErrorCommon(input, cause);
sb.append("Steps: ").append(input.steps).append('\n');
sb.append("Values:\n");
- sb.append("\tState: ").append(state).append(": ").append(state == null ? "unknown type" : state.getClass().getCanonicalName()).append('\n');
+ String stateStr = state == null ? null : state.toString().replace("\n", "\n\t\t");
+ sb.append("\tState: ").append(stateStr).append(": ").append(state == null ? "unknown type" : state.getClass().getCanonicalName()).append('\n');
sb.append("\tHistory:").append('\n');
+ int idx = 0;
for (var event : history)
- sb.append("\t\t").append(event).append('\n');
+ sb.append("\t\t").append(++idx).append(": ").append(event).append('\n');
return sb.toString();
}
@@ -415,6 +404,8 @@ public class Property
public static class StatefulBuilder extends Common<StatefulBuilder>
{
protected int steps = 1000;
+ @Nullable
+ protected Duration stepTimeout = null;
public StatefulBuilder()
{
@@ -427,6 +418,12 @@ public class Property
return this;
}
+ public StatefulBuilder withStepTimeout(Duration duration)
+ {
+ stepTimeout = duration;
+ return this;
+ }
+
@SuppressWarnings("rawtypes")
public <State, SystemUnderTest> void check(Commands<State, SystemUnderTest> commands)
{
@@ -454,16 +451,35 @@ public class Property
throw new IllegalArgumentException("Unable to find next command");
cmd = cmdGen.next(rs);
}
- history.add(cmd.detailed(state));
- Object stateResult = cmd.apply(state);
- cmd.checkPostconditions(state, stateResult,
- sut, cmd.run(sut));
+ if (cmd instanceof MultistepCommand)
+ {
+ for (Command<State, SystemUnderTest, ?> sub : ((MultistepCommand<State, SystemUnderTest>) cmd))
+ {
+ history.add(sub.detailed(state));
+ process(sub, state, sut, history.size());
+ }
+ }
+ else
+ {
+ history.add(cmd.detailed(state));
+ process(cmd, state, sut, history.size());
+ }
}
+ commands.destroySut(sut, null);
+ commands.destroyState(state, null);
}
- finally
+ catch (Throwable t)
{
- commands.destroySut(sut);
- commands.destroyState(state);
+ try
+ {
+ commands.destroySut(sut, t);
+ commands.destroyState(state, t);
+ }
+ catch (Throwable t2)
+ {
+ t.addSuppressed(t2);
+ }
+ throw t;
}
}
catch (Throwable t)
@@ -477,6 +493,16 @@ public class Property
}
}
}
+
+ private <State, SystemUnderTest> void process(Command cmd, State state, SystemUnderTest sut, int id) throws Throwable
+ {
+ if (stepTimeout == null)
+ {
+ cmd.process(state, sut);
+ return;
+ }
+ TimeoutUtils.runBlocking(stepTimeout, "Stateful Step " + id, () -> cmd.process(state, sut));
+ }
}
public enum PreCheckResult { Ok, Ignore }
@@ -488,6 +514,112 @@ public class Property
default void checkPostconditions(State state, Result expected,
SystemUnderTest sut, Result actual) throws Throwable {}
default String detailed(State state) {return this.toString();}
+ default void process(State state, SystemUnderTest sut) throws Throwable
+ {
+ checkPostconditions(state, apply(state),
+ sut, run(sut));
+ }
+ }
+
+ public static <State, SystemUnderTest> MultistepCommand<State, SystemUnderTest> multistep(Command<State, SystemUnderTest, ?>... cmds)
+ {
+ return multistep(Arrays.asList(cmds));
+ }
+
+ public static <State, SystemUnderTest> MultistepCommand<State, SystemUnderTest> multistep(List<Command<State, SystemUnderTest, ?>> cmds)
+ {
+ List<Command<State, SystemUnderTest, ?>> result = new ArrayList<>(cmds.size());
+ for (Command<State, SystemUnderTest, ?> c : cmds)
+ {
+ if (c instanceof MultistepCommand) result.addAll(flatten((MultistepCommand<State, SystemUnderTest>) c));
+ else result.add(c);
+ }
+ return result::iterator;
+ }
+
+ private static <State, SystemUnderTest> Collection<? extends Command<State, SystemUnderTest, ?>> flatten(MultistepCommand<State, SystemUnderTest> mc)
+ {
+ List<Command<State, SystemUnderTest, ?>> result = new ArrayList<>();
+ for (Command<State, SystemUnderTest, ?> c : mc)
+ {
+ if (c instanceof MultistepCommand) result.addAll(flatten((MultistepCommand<State, SystemUnderTest>) c));
+ else result.add(c);
+ }
+ return result;
+ }
+
+ public interface MultistepCommand<State, SystemUnderTest> extends Command<State, SystemUnderTest, Object>, Iterable<Command<State, SystemUnderTest, ?>>
+ {
+ @Override
+ default PreCheckResult checkPreconditions(State state)
+ {
+ for (Command<State, SystemUnderTest, ?> cmd : this)
+ {
+ PreCheckResult result = cmd.checkPreconditions(state);
+ if (result != PreCheckResult.Ok) return result;
+ }
+ return PreCheckResult.Ok;
+ }
+
+ @Override
+ default Object apply(State state) throws Throwable
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ default Object run(SystemUnderTest sut) throws Throwable
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ default void checkPostconditions(State state, Object expected, SystemUnderTest sut, Object actual) throws Throwable
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ default String detailed(State state)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ default void process(State state, SystemUnderTest sut) throws Throwable
+ {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ public static <State, SystemUnderTest, Result> Command<State, SystemUnderTest, Result> ignoreCommand()
+ {
+ return new Command<>()
+ {
+ @Override
+ public PreCheckResult checkPreconditions(State state)
+ {
+ return PreCheckResult.Ignore;
+ }
+
+ @Override
+ public Result apply(State state) throws Throwable
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Result run(SystemUnderTest sut) throws Throwable
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String detailed(State state)
+ {
+ throw new UnsupportedOperationException();
+ }
+ };
}
public interface UnitCommand<State, SystemUnderTest> extends Command<State, SystemUnderTest, Void>
@@ -510,12 +642,276 @@ public class Property
}
}
+ public interface StateOnlyCommand<State> extends UnitCommand<State, Void>
+ {
+ @Override
+ default void runUnit(Void sut) throws Throwable {}
+ }
+
+ public static class SimpleCommand<State> implements StateOnlyCommand<State>
+ {
+ private final Function<State, String> name;
+ private final Consumer<State> fn;
+
+ public SimpleCommand(String name, Consumer<State> fn)
+ {
+ this.name = ignore -> name;
+ this.fn = fn;
+ }
+
+ public SimpleCommand(Function<State, String> name, Consumer<State> fn)
+ {
+ this.name = name;
+ this.fn = fn;
+ }
+
+ @Override
+ public String detailed(State state)
+ {
+ return name.apply(state);
+ }
+
+ @Override
+ public void applyUnit(State state)
+ {
+ fn.accept(state);
+ }
+ }
+
public interface Commands<State, SystemUnderTest>
{
Gen<State> genInitialState() throws Throwable;
SystemUnderTest createSut(State state) throws Throwable;
- default void destroyState(State state) throws Throwable {}
- default void destroySut(SystemUnderTest sut) throws Throwable {}
+ default void destroyState(State state, @Nullable Throwable cause) throws Throwable {}
+ default void destroySut(SystemUnderTest sut, @Nullable Throwable cause) throws Throwable {}
Gen<Command<State, SystemUnderTest, ?>> commands(State state) throws Throwable;
}
+
+ public static <State, SystemUnderTest> CommandsBuilder<State, SystemUnderTest> commands(Supplier<Gen<State>> stateGen, Function<State, SystemUnderTest> sutFactory)
+ {
+ return new CommandsBuilder<>(stateGen, sutFactory);
+ }
+
+ public static <State> CommandsBuilder<State, Void> commands(Supplier<Gen<State>> stateGen)
+ {
+ return new CommandsBuilder<>(stateGen, ignore -> null);
+ }
+
+ public static class CommandsBuilder<State, SystemUnderTest>
+ {
+ public interface Setup<State, SystemUnderTest>
+ {
+ Command<State, SystemUnderTest, ?> setup(RandomSource rs, State state);
+ }
+ private final Supplier<Gen<State>> stateGen;
+ private final Function<State, SystemUnderTest> sutFactory;
+ private final Map<Setup<State, SystemUnderTest>, Integer> knownWeights = new LinkedHashMap<>();
+ @Nullable
+ private Set<Setup<State, SystemUnderTest>> unknownWeights = null;
+ private Gen.IntGen unknownWeightGen = Gens.ints().between(1, 10);
+ @Nullable
+ private FailingConsumer<State> preCommands = null;
+ @Nullable
+ private FailingBiConsumer<State, Throwable> destroyState = null;
+ @Nullable
+ private FailingBiConsumer<SystemUnderTest, Throwable> destroySut = null;
+
+ public CommandsBuilder(Supplier<Gen<State>> stateGen, Function<State, SystemUnderTest> sutFactory)
+ {
+ this.stateGen = stateGen;
+ this.sutFactory = sutFactory;
+ }
+
+ public CommandsBuilder<State, SystemUnderTest> preCommands(FailingConsumer<State> preCommands)
+ {
+ this.preCommands = preCommands;
+ return this;
+ }
+
+ public CommandsBuilder<State, SystemUnderTest> destroyState(FailingConsumer<State> destroyState)
+ {
+ return destroyState((success, failure) -> {
+ if (failure == null)
+ destroyState.accept(success);
+ });
+ }
+
+ public CommandsBuilder<State, SystemUnderTest> destroyState(FailingBiConsumer<State, Throwable> destroyState)
+ {
+ this.destroyState = destroyState;
+ return this;
+ }
+
+ public CommandsBuilder<State, SystemUnderTest> destroySut(FailingConsumer<SystemUnderTest> destroySut)
+ {
+ return destroySut((success, failure) -> {
+ if (failure == null)
+ destroySut.accept(success);
+ });
+ }
+
+ public CommandsBuilder<State, SystemUnderTest> destroySut(FailingBiConsumer<SystemUnderTest, Throwable> destroySut)
+ {
+ this.destroySut = destroySut;
+ return this;
+ }
+
+ public CommandsBuilder<State, SystemUnderTest> add(int weight, Command<State, SystemUnderTest, ?> cmd)
+ {
+ return add(weight, (i1, i2) -> cmd);
+ }
+
+ public CommandsBuilder<State, SystemUnderTest> add(int weight, Gen<Command<State, SystemUnderTest, ?>> cmd)
+ {
+ return add(weight, (rs, state) -> cmd.next(rs));
+ }
+
+ public CommandsBuilder<State, SystemUnderTest> add(int weight, Setup<State, SystemUnderTest> cmd)
+ {
+ knownWeights.put(cmd, weight);
+ return this;
+ }
+
+ public CommandsBuilder<State, SystemUnderTest> add(Command<State, SystemUnderTest, ?> cmd)
+ {
+ return add((i1, i2) -> cmd);
+ }
+
+ public CommandsBuilder<State, SystemUnderTest> add(Gen<Command<State, SystemUnderTest, ?>> cmd)
+ {
+ return add((rs, state) -> cmd.next(rs));
+ }
+
+ public CommandsBuilder<State, SystemUnderTest> add(Setup<State, SystemUnderTest> cmd)
+ {
+ if (unknownWeights == null)
+ unknownWeights = new LinkedHashSet<>();
+ unknownWeights.add(cmd);
+ return this;
+ }
+
+ public CommandsBuilder<State, SystemUnderTest> addIf(Predicate<State> predicate, Gen<Command<State, SystemUnderTest, ?>> cmd)
+ {
+ return add((rs, state) -> {
+ if (!predicate.test(state)) return ignoreCommand();
+ return cmd.next(rs);
+ });
+ }
+
+ public CommandsBuilder<State, SystemUnderTest> addIf(Predicate<State> predicate, Setup<State, SystemUnderTest> cmd)
+ {
+ return add((rs, state) -> {
+ if (!predicate.test(state)) return ignoreCommand();
+ return cmd.setup(rs, state);
+ });
+ }
+
+ public CommandsBuilder<State, SystemUnderTest> addAllIf(Predicate<State> predicate, Consumer<IfBuilder<State, SystemUnderTest>> sub)
+ {
+ sub.accept(new IfBuilder<>()
+ {
+ @Override
+ public IfBuilder<State, SystemUnderTest> add(Setup<State, SystemUnderTest> cmd)
+ {
+ CommandsBuilder.this.addIf(predicate, cmd);
+ return this;
+ }
+ });
+ return this;
+ }
+
+ public interface IfBuilder<State, SystemUnderTest>
+ {
+ IfBuilder<State, SystemUnderTest> add(Setup<State, SystemUnderTest> cmd);
+ }
+
+ public CommandsBuilder<State, SystemUnderTest> unknownWeight(Gen.IntGen unknownWeightGen)
+ {
+ this.unknownWeightGen = Objects.requireNonNull(unknownWeightGen);
+ return this;
+ }
+
+ public Commands<State, SystemUnderTest> build()
+ {
+ Gen<Setup<State, SystemUnderTest>> commandsGen;
+ if (unknownWeights == null)
+ {
+ commandsGen = Gens.pick(new LinkedHashMap<>(knownWeights));
+ }
+ else
+ {
+ class DynamicWeightsGen implements Gen<Setup<State, SystemUnderTest>>, Gens.Reset
+ {
+ Gen<Setup<State, SystemUnderTest>> gen;
+ @Override
+ public Setup<State, SystemUnderTest> next(RandomSource rs)
+ {
+ if (gen == null)
+ {
+ // create random weights
+ LinkedHashMap<Setup<State, SystemUnderTest>, Integer> clone = new LinkedHashMap<>(knownWeights);
+ for (Setup<State, SystemUnderTest> s : unknownWeights)
+ clone.put(s, unknownWeightGen.nextInt(rs));
+ gen = Gens.pick(clone);
+ }
+ return gen.next(rs);
+ }
+
+ @Override
+ public void reset()
+ {
+ gen = null;
+ }
+ }
+ commandsGen = new DynamicWeightsGen();
+ }
+ return new Commands<>()
+ {
+ @Override
+ public Gen<State> genInitialState() throws Throwable
+ {
+ return stateGen.get();
+ }
+
+ @Override
+ public SystemUnderTest createSut(State state) throws Throwable
+ {
+ return sutFactory.apply(state);
+ }
+
+ @Override
+ public Gen<Command<State, SystemUnderTest, ?>> commands(State state) throws Throwable
+ {
+ if (preCommands != null)
+ preCommands.accept(state);
+ return commandsGen.map((rs, setup) -> setup.setup(rs, state));
+ }
+
+ @Override
+ public void destroyState(State state, @Nullable Throwable cause) throws Throwable
+ {
+ Gens.Reset.tryReset(commandsGen);
+ if (destroyState != null)
+ destroyState.accept(state, cause);
+ }
+
+ @Override
+ public void destroySut(SystemUnderTest sut, @Nullable Throwable cause) throws Throwable
+ {
+ if (destroySut != null)
+ destroySut.accept(sut, cause);
+ }
+ };
+ }
+
+ public interface FailingConsumer<T>
+ {
+ void accept(T value) throws Throwable;
+ }
+
+ public interface FailingBiConsumer<A, B>
+ {
+ void accept(A a, B b) throws Throwable;
+ }
+ }
}
diff --git a/accord-core/src/test/java/accord/utils/SeedProvider.java b/accord-core/src/test/java/accord/utils/SeedProvider.java
new file mode 100644
index 00000000..9c7858da
--- /dev/null
+++ b/accord-core/src/test/java/accord/utils/SeedProvider.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package accord.utils;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Utility class for creating seeds. This class mostly matches the semantics of {@link java.util.Random} but makes the logic work
+ * for any random source. This class should be used in replacement of most seed methods, and should always replace {@link java.util.concurrent.ThreadLocalRandom}
+ * as that randomness will have a bias twords the same seed after a restart (if you rerun randomized tests by restarting
+ * the JVM you will run with the same seed over and over again).
+ */
+public class SeedProvider
+{
+ public static final SeedProvider instance = new SeedProvider();
+ private final AtomicLong seedUniquifier = new AtomicLong(8682522807148012L);
+
+ private long seedUniquifier()
+ {
+ // L'Ecuyer, "Tables of Linear Congruential Generators of
+ // Different Sizes and Good Lattice Structure", 1999
+ for (; ; )
+ {
+ long current = seedUniquifier.get();
+ long next = current * 1181783497276652981L;
+ if (seedUniquifier.compareAndSet(current, next))
+ return next;
+ }
+ }
+
+ public long nextSeed()
+ {
+ return seedUniquifier() ^ System.nanoTime();
+ }
+}
diff --git a/accord-core/src/test/java/accord/utils/async/TimeoutUtils.java b/accord-core/src/test/java/accord/utils/async/TimeoutUtils.java
new file mode 100644
index 00000000..9754d604
--- /dev/null
+++ b/accord-core/src/test/java/accord/utils/async/TimeoutUtils.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package accord.utils.async;
+
+import java.time.Duration;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public class TimeoutUtils
+{
+ public interface FailingRunnable
+ {
+ void run() throws Throwable;
+ }
+
+ public static void runBlocking(Duration timeout, String threadName, FailingRunnable fn) throws ExecutionException, InterruptedException, TimeoutException
+ {
+ AsyncResult.Settable<?> promise = AsyncResults.settable();
+ Thread t = new Thread(() -> {
+ try
+ {
+ fn.run();
+ promise.setSuccess(null);
+ }
+ catch (Throwable e)
+ {
+ promise.setFailure(e);
+ }
+ });
+ t.setName(threadName);
+ t.setDaemon(true);
+ t.start();
+ try
+ {
+ AsyncChains.getBlocking(promise, timeout.toNanos(), TimeUnit.NANOSECONDS);
+ }
+ catch (InterruptedException e)
+ {
+ t.interrupt();
+ throw e;
+ }
+ catch (TimeoutException e)
+ {
+ t.interrupt();
+ throw e;
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org