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