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 7205f68a Add documentation for Accord's property testing (#196)
7205f68a is described below

commit 7205f68a25f5b925a08cc58547981ce2bf485f8f
Author: dcapwell <dcapw...@apache.org>
AuthorDate: Thu May 29 15:23:03 2025 -0700

    Add documentation for Accord's property testing (#196)
    
    patch by David Capwell; reviewed by Jordan West, Stefan Miklosovic for 
CASSANDRA-20684
---
 accord-core/src/test/java/accord/utils/Gen.java    |  56 ++-
 accord-core/src/test/java/accord/utils/Gens.java   | 433 ++++++++++++++++++++-
 .../src/test/java/accord/utils/Property.java       |  77 +++-
 accord-core/src/test/java/accord/utils/README.md   | 318 +++++++++++++++
 4 files changed, 859 insertions(+), 25 deletions(-)

diff --git a/accord-core/src/test/java/accord/utils/Gen.java 
b/accord-core/src/test/java/accord/utils/Gen.java
index 357c197a..822a5e14 100644
--- a/accord-core/src/test/java/accord/utils/Gen.java
+++ b/accord-core/src/test/java/accord/utils/Gen.java
@@ -34,6 +34,17 @@ import java.util.stream.IntStream;
 import java.util.stream.LongStream;
 import java.util.stream.Stream;
 
+/**
+ * A generator (gen for short) interface that produces values using a {@link 
RandomSource}.
+ * <p>
+ * This interface provides a way to generate random values of type {@code A} 
and transform them
+ * using various functional operators like {@link #map(Function)}, {@link 
#filter(Predicate)}, and {@link #flatMap(Function)}.
+ * <p>
+ * Specialized versions exist for primitive types ({@link IntGen}, {@link 
LongGen}) to avoid boxing/unboxing overhead.
+ *
+ * @param <A> The type of values this generator produces
+ */
+@SuppressWarnings("unused")
 public interface Gen<A>
 {
     /**
@@ -45,6 +56,22 @@ public interface Gen<A>
         return fn;
     }
 
+    /**
+     * Core method of the {@link Gen} interface that produces a value of type 
{@code A} using the provided {@link RandomSource}.
+     * <p>
+     * The semantics of this method are implementation-specific. Some 
important considerations:
+     * <ul>
+     * <li>Some implementations may return null values</li>
+     * <li>Implementations may return duplicate or identical values across 
calls</li>
+     * <li>Values may contain or be backed by shared mutable state</li>
+     * </ul>
+     * <p>
+     * Due to these implementation-specific behaviors, callers should avoid 
storing references to returned
+     * values for extended periods unless the specific generator's safety 
guarantees are known.
+     *
+     * @param random The random source to use for generating values
+     * @return A generated value of type {@code A}
+     */
     A next(RandomSource random);
 
     default <B> Gen<B> map(Function<? super A, ? extends B> fn)
@@ -77,6 +104,18 @@ public interface Gen<A>
         return rs -> mapper.apply(rs, this.next(rs)).next(rs);
     }
 
+    /**
+     * Creates a new generator that only produces values matching the provided 
predicate.
+     * <p>
+     * <strong>Warning:</strong> This method is unbounded and will loop 
indefinitely if the predicate
+     * never returns true for any generated value. Use with caution, 
especially with rare conditions.
+     * <p>
+     * For a bounded alternative that will stop after a specified number of 
attempts,
+     * see {@link #filter(int, Object, Predicate)}.
+     *
+     * @param fn the predicate function that values must satisfy
+     * @return a new generator that only produces values matching the predicate
+     */
     default Gen<A> filter(Predicate<A> fn)
     {
         Gen<A> self = this;
@@ -90,6 +129,19 @@ public interface Gen<A>
         };
     }
 
+    /**
+     * Creates a new bounded generator that only produces values matching the 
provided predicate.
+     * <p>
+     * Unlike the unbounded {@link #filter(Predicate)}, this method will make 
at most {@code maxAttempts}
+     * attempts to generate a value that satisfies the predicate. If no 
matching value is found within
+     * the specified number of attempts, it returns the provided {@code 
defaultValue} instead.
+     *
+     * @param maxAttempts the maximum number of generation attempts before 
falling back to the default value
+     * @param defaultValue the value to return if no matching value is found 
within maxAttempts
+     * @param fn the predicate function that values must satisfy
+     * @return a new bounded generator that produces values matching the 
predicate or the default value
+     * @throws IllegalArgumentException if maxAttempts is not positive
+     */
     default Gen<A> filter(int maxAttempts, A defaultValue, Predicate<A> fn)
     {
         Invariants.requireArgument(maxAttempts > 0, "Max attempts must be 
positive; given %d", maxAttempts);
@@ -172,7 +224,7 @@ public interface Gen<A>
         @Override
         default Gen.IntGen filter(Predicate<Integer> fn)
         {
-            return filterAsInt(i -> fn.test(i));
+            return filterAsInt(fn::test);
         }
 
         default IntSupplier asIntSupplier(RandomSource rs)
@@ -222,7 +274,7 @@ public interface Gen<A>
         @Override
         default Gen.LongGen filter(Predicate<Long> fn)
         {
-            return filterAsLong(i -> fn.test(i));
+            return filterAsLong(fn::test);
         }
 
         default LongSupplier asLongSupplier(RandomSource rs)
diff --git a/accord-core/src/test/java/accord/utils/Gens.java 
b/accord-core/src/test/java/accord/utils/Gens.java
index d3adac61..d9dcfb8e 100644
--- a/accord-core/src/test/java/accord/utils/Gens.java
+++ b/accord-core/src/test/java/accord/utils/Gens.java
@@ -26,15 +26,17 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.EnumMap;
+import java.util.EnumSet;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.NavigableSet;
 import java.util.Objects;
 import java.util.Set;
+import java.util.SortedMap;
+import java.util.SortedSet;
 import java.util.function.BooleanSupplier;
 import java.util.function.Function;
 import java.util.function.Supplier;
@@ -46,7 +48,9 @@ import com.google.common.collect.Iterables;
 
 import accord.utils.random.Picker;
 
-public class Gens {
+@SuppressWarnings("unused")
+public class Gens
+{
     private Gens() {
     }
 
@@ -65,6 +69,19 @@ public class Gens {
         return ignore -> constant.get();
     }
 
+    /**
+     * Creates a generator that randomly selects one of the provided 
generators on each execution, then calls {@link Gen#next(RandomSource)} on that 
chosen generator.
+     * <p>
+     * This method distributes the selection with equal probability. For 
weighted selection,
+     * see {@link #oneOf(Map)}.
+     *
+     * @param <T> the type of values to generate
+     * @param gens the generators to select from
+     * @return a generator that uses one of the provided generators for each 
value
+     * @throws IllegalArgumentException if gens is empty
+     */
+    @SuppressWarnings("unchecked")
+    @SafeVarargs
     public static <T> Gen<T> oneOf(Gen<? extends T>... gens)
     {
         switch (gens.length)
@@ -75,6 +92,18 @@ public class Gens {
         return oneOf(Arrays.asList(gens));
     }
 
+    /**
+     * Creates a generator that randomly selects one of the provided 
generators from a list on each execution, then calls {@link 
Gen#next(RandomSource)} on that chosen generator.
+     * <p>
+     * This method distributes the selection with equal probability. For 
weighted selection,
+     * see {@link #oneOf(Map)}.
+     *
+     * @param <T> the type of values to generate
+     * @param gens the list of generators to select from
+     * @return a generator that uses one of the provided generators for each 
value
+     * @throws IllegalArgumentException if gens is empty
+     */
+    @SuppressWarnings("unchecked")
     public static <T> Gen<T> oneOf(List<Gen<? extends T>> gens)
     {
         switch (gens.size())
@@ -85,17 +114,50 @@ public class Gens {
         return rs -> rs.pick(gens).next(rs);
     }
 
+    /**
+     * Creates a generator that randomly selects one of the provided 
generators based on weight, then calls {@link Gen#next(RandomSource)} on that 
chosen generator.
+     * <p>
+     * Each generator is selected with a probability proportional to its 
associated weight.
+     * Higher weights increase the likelihood of selection.
+     *
+     * @param <T> the type of values to generate
+     * @param values a map of generators to their respective weights
+     * @return a generator that uses one of the provided generators according 
to their weights
+     * @throws IllegalArgumentException if values is empty
+     */
     public static <T> Gen<T> oneOf(Map<Gen<T>, Integer> values)
     {
         Gen<Gen<T>> gen = pick(values);
         return rs -> gen.next(rs).next(rs);
     }
 
+    /**
+     * A builder for creating generators that randomly select from multiple 
generators with customizable weights.
+     *
+     * @see OneOfBuilder
+     * @return a oneOf builder to aid in creating a {@code Gen} of type {@code 
T}
+     * @param <T> the generator will return
+     */
     public static <T> OneOfBuilder<T> oneOf()
     {
         return new OneOfBuilder<>();
     }
 
+    /**
+     * A builder for creating generators that randomly select from multiple 
generators with customizable weights.
+     * <p>
+     * This builder allows constructing complex random selection logic with:
+     * <ul>
+     * <li>Weighted probabilities for different generators</li>
+     * <li>Dynamically determined weights at generation time (if {@link 
#build} is called, the weight will be {@code 1})</li>
+     * </ul>
+     * <p>
+     * The {@link #buildWithDynamicWeights()} method is particularly powerful 
as it creates
+     * a meta-generator that recalculates the weights for unweighted 
generators on each invocation,
+     * producing more varied randomization patterns than static weights.
+     *
+     * @param <T> the type of values the generators will produce
+     */
     public static class OneOfBuilder<T>
     {
         private final Map<Gen<T>, Integer> weighted = new LinkedHashMap<>();
@@ -128,8 +190,7 @@ public class Gens {
                 return i -> gen;
             }
             return rs -> {
-                Map<Gen<T>, Integer> commands = new LinkedHashMap<>();
-                commands.putAll(weighted);
+                Map<Gen<T>, Integer> commands = new LinkedHashMap<>(weighted);
                 for (var gen : unweighted)
                     commands.put(gen, unknownWeightGen.nextInt(rs));
                 var top = pick(commands);
@@ -139,8 +200,7 @@ public class Gens {
 
         public Gen<T> build()
         {
-            Map<Gen<T>, Integer> commands = new LinkedHashMap<>();
-            commands.putAll(weighted);
+            Map<Gen<T>, Integer> commands = new LinkedHashMap<>(weighted);
             for (var gen : unweighted)
                 commands.put(gen, 1);
             var top = pick(commands);
@@ -148,40 +208,96 @@ public class Gens {
         }
     }
 
+    /**
+     * Creates a generator that randomly selects one value from an array.
+     * <p>
+     * Each value has an equal probability of being selected.
+     *
+     * @param ts the array of values to select from
+     * @return a generator that produces random values from the given array
+     * @throws IllegalArgumentException if the array is empty
+     */
     public static Gen.IntGen pickInt(int... ts)
     {
+        Invariants.requireArgument(ts.length > 0, "Unable to pick from an 
empty array");
         return rs -> ts[rs.nextInt(0, ts.length)];
     }
 
+    /**
+     * Creates a generator that randomly selects one value from an array.
+     * <p>
+     * Each value has an equal probability of being selected.
+     *
+     * @param <T> the type of values to generate
+     * @param ts the array of values to select from
+     * @return a generator that produces random values from the given array
+     * @throws IllegalArgumentException if the array is empty
+     */
+    @SafeVarargs
     public static <T> Gen<T> pick(T... ts)
     {
+        Invariants.requireArgument(ts.length > 0, "Unable to pick from an 
empty array");
         return pick(Arrays.asList(ts));
     }
 
+    /**
+     * Creates a generator that randomly selects one value from a list.
+     * <p>
+     * Each value has an equal probability of being selected.
+     *
+     * @param <T> the type of values to generate
+     * @param ts the list of values to select from
+     * @return a generator that produces random values from the given list
+     * @throws IllegalArgumentException if the list is empty
+     */
     public static <T> Gen<T> pick(List<T> ts)
     {
+        Invariants.requireArgument(!ts.isEmpty(), "Unable to pick from an 
empty collection");
         Gen.IntGen offset = ints().between(0, ts.size() - 1);
         return rs -> ts.get(offset.nextInt(rs));
     }
 
+    /**
+     * Creates a generator that randomly selects one value from a set. When 
the set does not have deterministic
+     * iteration ordering, then the values will be sorted based off their 
natural ordering first.
+     * <p>
+     * Each value has an equal probability of being selected.
+     *
+     * @param <T> the type of values to generate
+     * @param set the set of values to select from
+     * @return a generator that produces random values from the given set
+     * @throws IllegalArgumentException if the set is empty
+     */
     public static <T extends Comparable<T>> Gen<T> pick(Set<T> set)
     {
+        Invariants.requireArgument(!set.isEmpty(), "Unable to pick from an 
empty collection");
         List<T> list = new ArrayList<>(set);
         // Non-ordered sets may have different iteration order on different 
environments, which would make a seed produce different histories!
         // To avoid such a problem, make sure to apply a deterministic 
function (sort).
-        if (!(set instanceof NavigableSet))
+        if (!isIterationSafe(set))
             list.sort(Comparator.naturalOrder());
         return pick(list);
     }
 
+    /**
+     * Creates a generator that randomly selects one value from a weighted map.
+     * <p>
+     * Each value is selected with a probability proportional to its 
associated weight.
+     * Higher weights increase the likelihood of selection.
+     * <p>
+     * For deterministic behavior, this method requires maps with 
deterministic iteration order
+     * like {@link java.util.EnumMap} or {@link java.util.LinkedHashMap}.
+     *
+     * @param <T> the type of values to generate
+     * @param values a map of values to their respective weights
+     * @return a generator that produces values according to their weights
+     * @throws IllegalArgumentException if values is empty or doesn't have 
deterministic iteration order
+     */
     public static <T> Gen<T> pick(Map<T, Integer> values)
     {
         if (values == null || values.isEmpty())
             throw new IllegalArgumentException("values is empty");
-        // if 2 values have the same weight we need some way to tie-break, but 
that isn't always possible...
-        // 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());
+        checkIterationSafe("pick(Map)", values);
         if (values.size() == 1)
             return 
constant(Objects.requireNonNull(Iterables.getFirst(values.keySet(), null)));
         double totalWeight = 
values.values().stream().mapToDouble(Integer::intValue).sum();
@@ -205,6 +321,41 @@ public class Gens {
         };
     }
 
+    private static boolean isIterationSafe(Map<?, ?> values)
+    {
+        return values instanceof EnumMap
+                || values instanceof LinkedHashMap
+                || values instanceof SortedMap;
+    }
+
+    private static boolean isIterationSafe(Set<?> values)
+    {
+        return values instanceof EnumSet
+               || values instanceof LinkedHashSet
+               || values instanceof SortedSet;
+    }
+
+    private static void checkIterationSafe(String name, Map<?, ?> values)
+    {
+        if (!isIterationSafe(values))
+            throw new IllegalArgumentException(name + " requires a map with 
deterministic iteration; given " + values.getClass());
+    }
+
+    /**
+     * Creates a generator that selects values from an array using a Zipfian 
distribution.
+     * <p>
+     * In a Zipfian distribution, the probability of selecting an element 
decreases as its index increases.
+     * The first element has the highest probability of being selected, the 
second element has the second-highest
+     * probability, and so on. This is useful for generating skewed data where 
some values are much more common than
+     * others.
+     * <p>
+     * The weights follow a power law where {@code weight = base / (i+1)}, 
creating a harmonic series.
+     *
+     * @see <a href="https://en.wikipedia.org/wiki/Zipf%27s_law";>Wikipedia</a>
+     * @param array the array to select from
+     * @return a generator that produces values from the array following a 
Zipfian distribution
+     * @throws IllegalArgumentException if the array is null or empty
+     */
     public static Gen.IntGen pickZipf(int[] array)
     {
         if (array == null || array.length == 0)
@@ -230,6 +381,21 @@ public class Gens {
         };
     }
 
+    /**
+     * Creates a generator that selects values from an array using a Zipfian 
distribution.
+     * <p>
+     * In a Zipfian distribution, the probability of selecting an element 
decreases as its index increases.
+     * The first element has the highest probability of being selected, the 
second element has the second-highest
+     * probability, and so on. This is useful for generating skewed data where 
some values are much more common than
+     * others.
+     * <p>
+     * The weights follow a power law where {@code weight = base / (i+1)}, 
creating a harmonic series.
+     *
+     * @see <a href="https://en.wikipedia.org/wiki/Zipf%27s_law";>Wikipedia</a>
+     * @param array the array to select from
+     * @return a generator that produces values from the array following a 
Zipfian distribution
+     * @throws IllegalArgumentException if the array is null or empty
+     */
     public static Gen.LongGen pickZipf(long[] array)
     {
         if (array == null || array.length == 0)
@@ -255,11 +421,42 @@ public class Gens {
         };
     }
 
+    /**
+     * Creates a generator that selects values from an array using a Zipfian 
distribution.
+     * <p>
+     * In a Zipfian distribution, the probability of selecting an element 
decreases as its index increases.
+     * The first element has the highest probability of being selected, the 
second element has the second-highest
+     * probability, and so on. This is useful for generating skewed data where 
some values are much more common than
+     * others.
+     * <p>
+     * The weights follow a power law where {@code weight = base / (i+1)}, 
creating a harmonic series.
+     *
+     * @see <a href="https://en.wikipedia.org/wiki/Zipf%27s_law";>Wikipedia</a>
+     * @param array the array to select from
+     * @return a generator that produces values from the array following a 
Zipfian distribution
+     * @throws IllegalArgumentException if the array is null or empty
+     */
+    @SafeVarargs
     public static <T> Gen<T> pickZipf(T... array)
     {
         return pickZipf(Arrays.asList(array));
     }
 
+    /**
+     * Creates a generator that selects values from a list using a Zipfian 
distribution.
+     * <p>
+     * In a Zipfian distribution, the probability of selecting an element 
decreases as its index increases.
+     * The first element has the highest probability of being selected, the 
second element has the second-highest
+     * probability, and so on. This is useful for generating skewed data where 
some values are much more common than
+     * others.
+     * <p>
+     * The weights follow a power law where {@code weight = base / (i+1)}, 
creating a harmonic series.
+     *
+     * @see <a href="https://en.wikipedia.org/wiki/Zipf%27s_law";>Wikipedia</a>
+     * @param array the list to select from
+     * @return a generator that produces values from the list following a 
Zipfian distribution
+     * @throws IllegalArgumentException if the list is null or empty
+     */
     public static <T> Gen<T> pickZipf(List<T> array)
     {
         if (array == null || array.isEmpty())
@@ -285,10 +482,57 @@ public class Gens {
         };
     }
 
+    /**
+     * Creates a generator that produces a subset of the input list.
+     * <p>
+     * Elements are selected randomly from the input list without modifying 
it; the subset returned may not reflect
+     * the original order.
+     *
+     * @param <T> the type of elements in the list
+     * @param input the source list to select elements from
+     * @return a generator that produces variable-sized random subsets of the 
input list
+     * @throws IndexOutOfBoundsException if sizeGen produces a value outside 
the list's range
+     */
     public static <T> Gen<List<T>> select(List<T> input)
     {
+        return select(input, rs -> rs.nextInt(0, input.size() + 1));
+    }
+
+    /**
+     * Creates a generator that produces a subset of the input list.
+     * <p>
+     * Elements are selected randomly from the input list without modifying 
it; the subset returned may not reflect
+     * the original order.
+     *
+     * @param <T> the type of elements in the list
+     * @param input the source list to select elements from
+     * @param size fixed length of how many elements to return
+     * @return a generator that produces fixed-sized random subsets of the 
input list
+     * @throws IndexOutOfBoundsException if sizeGen produces a value outside 
the list's range
+     */
+    public static <T> Gen<List<T>> select(List<T> input, int size)
+    {
+        return select(input, ignore -> size);
+    }
+
+    /**
+     * Creates a generator that produces a subset of the input list.
+     * <p>
+     * Elements are selected randomly from the input list without modifying 
it; the subset returned may not reflect
+     * the original order.
+     *
+     * @param <T> the type of elements in the list
+     * @param input the source list to select elements from
+     * @param sizeGen a generator that determines how many elements to select
+     * @return a generator that produces variable-sized random subsets of the 
input list
+     * @throws IndexOutOfBoundsException if sizeGen produces a value outside 
the list's range
+     */
+    public static <T> Gen<List<T>> select(List<T> input, Gen.IntGen sizeGen)
+    {
+        if (input.isEmpty()) return constant(input);
         return rs -> {
-            int size = rs.nextInt(0, input.size() + 1);
+            int size = sizeGen.nextInt(rs);
+            Invariants.requireIndexInBounds(input.size(), 0, size);
             List<T> remaining = new ArrayList<>(input);
             List<T> list = new ArrayList<>(size);
             for (int i = 0; i < size; i++)
@@ -336,6 +580,27 @@ public class Gens {
         return i;
     }
 
+    /**
+     * Creates a generator that uses different distribution strategies for 
selecting items from a range.  The range is
+     * broken up into {@code numBuckets} sub ranges, and the bucket will be 
selected using the distribution strategies.
+     * <p>
+     * When the top level generator is called it selects what distribution to 
use, and returns a generator of {@code int}
+     * that selects from the input using that distribution.
+     * <p>
+     * This method does not document what distributions it can select from, 
that way it is free to change (add/remove)
+     * over time; it only maintains the property that the distribution may 
change between calls to the top level generator.
+     * <p>
+     * Some sample distribution strategies:
+     * <ul>
+     *     <li>Uniform distribution - Each item has equal chance of 
selection</li>
+     *     <li>Zipfian distribution - Early items are heavily favored, with 
probability decreasing by index</li>
+     * </ul>
+     *
+     * @param minInclusive value inclusive
+     * @param maxExclusive value exclusive
+     * @param numBuckets how many sub-ranges to create for the top level range
+     * @return a generator of different distribution strategies
+     */
     public static Gen<Gen.IntGen> mixedDistribution(int minInclusive, int 
maxExclusive, int numBuckets)
     {
         int domainSize = (maxExclusive - minInclusive);
@@ -365,7 +630,7 @@ public class Gens {
                 }
                 case 1: // median biased
                 {
-                    int medians[] = new int[bucket.length];
+                    int[] medians = new int[bucket.length];
                     for (int i = 0; i < medians.length; i++)
                     {
                         int start = bucket[i];
@@ -386,6 +651,25 @@ public class Gens {
         };
     }
 
+    /**
+     * Creates a generator that uses different distribution strategies for 
selecting items from a range.
+     * <p>
+     * When the top level generator is called it selects what distribution to 
use, and returns a generator of {@code int}
+     * that selects from the input using that distribution.
+     * <p>
+     * This method does not document what distributions it can select from, 
that way it is free to change (add/remove)
+     * over time; it only maintains the property that the distribution may 
change between calls to the top level generator.
+     * <p>
+     * Some sample distribution strategies:
+     * <ul>
+     *     <li>Uniform distribution - Each item has equal chance of 
selection</li>
+     *     <li>Zipfian distribution - Early items are heavily favored, with 
probability decreasing by index</li>
+     * </ul>
+     *
+     * @param minInclusive value inclusive
+     * @param maxExclusive value exclusive
+     * @return a generator of different distribution strategies
+     */
     public static Gen<Gen.IntGen> mixedDistribution(int minInclusive, int 
maxExclusive)
     {
         int domainSize = (maxExclusive - minInclusive + 1);
@@ -448,6 +732,25 @@ public class Gens {
         return array;
     }
 
+    /**
+     * Creates a generator that uses different distribution strategies for 
selecting items from a range.
+     * <p>
+     * When the top level generator is called it selects what distribution to 
use, and returns a generator of {@code int}
+     * that selects from the input using that distribution.
+     * <p>
+     * This method does not document what distributions it can select from, 
that way it is free to change (add/remove)
+     * over time; it only maintains the property that the distribution may 
change between calls to the top level generator.
+     * <p>
+     * Some sample distribution strategies:
+     * <ul>
+     *     <li>Uniform distribution - Each item has equal chance of 
selection</li>
+     *     <li>Zipfian distribution - Early items are heavily favored, with 
probability decreasing by index</li>
+     * </ul>
+     *
+     * @param minInclusive value inclusive
+     * @param maxExclusive value exclusive
+     * @return a generator of different distribution strategies
+     */
     public static Gen<Gen.LongGen> mixedDistribution(long minInclusive, long 
maxExclusive)
     {
         long domainSize = (maxExclusive - minInclusive + 1);
@@ -511,11 +814,50 @@ public class Gens {
         return array;
     }
 
+    /**
+     * Creates a generator that uses different distribution strategies for 
selecting items from an array.
+     * <p>
+     * When the top level generator is called it selects what distribution to 
use, and returns a generator of {@code T}
+     * that selects from the input using that distribution.
+     * <p>
+     * This method does not document what distributions it can select from, 
that way it is free to change (add/remove)
+     * over time; it only maintains the property that the distribution may 
change between calls to the top level generator.
+     * <p>
+     * Some sample distribution strategies:
+     * <ul>
+     *     <li>Uniform distribution - Each item has equal chance of 
selection</li>
+     *     <li>Zipfian distribution - Early items are heavily favored, with 
probability decreasing by index</li>
+     * </ul>
+     *
+     * @param <T> the type of elements in the array
+     * @param list the array of values to select from
+     * @return a generator of different distribution strategies
+     */
+    @SafeVarargs
     public static <T> Gen<Gen<T>> mixedDistribution(T... list)
     {
         return mixedDistribution(Arrays.asList(list));
     }
 
+    /**
+     * Creates a generator that uses different distribution strategies for 
selecting items from a list.
+     * <p>
+     * When the top level generator is called it selects what distribution to 
use, and returns a generator of {@code T}
+     * that selects from the input using that distribution.
+     * <p>
+     * This method does not document what distributions it can select from, 
that way it is free to change (add/remove)
+     * over time; it only maintains the property that the distribution may 
change between calls to the top level generator.
+     * <p>
+     * Some sample distribution strategies:
+     * <ul>
+     *     <li>Uniform distribution - Each item has equal chance of 
selection</li>
+     *     <li>Zipfian distribution - Early items are heavily favored, with 
probability decreasing by index</li>
+     * </ul>
+     *
+     * @param <T> the type of elements in the list
+     * @param list the list of values to select from
+     * @return a generator of different distribution strategies
+     */
     public static <T> Gen<Gen<T>> mixedDistribution(List<T> list)
     {
         return rs -> {
@@ -542,7 +884,25 @@ public class Gens {
         };
     }
 
-    public static <T> Gen<Gen.IntGen> mixedDistribution(int[] list)
+    /**
+     * Creates a generator that uses different distribution strategies for 
selecting items from an array.
+     * <p>
+     * When the top level generator is called it selects what distribution to 
use, and returns a generator of {@code int}
+     * that selects from the input using that distribution.
+     * <p>
+     * This method does not document what distributions it can select from, 
that way it is free to change (add/remove)
+     * over time; it only maintains the property that the distribution may 
change between calls to the top level generator.
+     * <p>
+     * Some sample distribution strategies:
+     * <ul>
+     *     <li>Uniform distribution - Each item has equal chance of 
selection</li>
+     *     <li>Zipfian distribution - Early items are heavily favored, with 
probability decreasing by index</li>
+     * </ul>
+     *
+     * @param list the array of values to select from
+     * @return a generator of different distribution strategies
+     */
+    public static Gen<Gen.IntGen> mixedDistribution(int[] list)
     {
         return rs -> {
             switch (rs.nextInt(0, 4))
@@ -654,15 +1014,31 @@ public class Gens {
             return RandomSource::nextBoolean;
         }
 
+        /**
+         * Creates a generator that produces boolean values with a target 
ratio of true values,
+         * and with true values appearing in contiguous runs of random length.
+         * <p>
+         * This is a stateful generator (not thread safe) that maintains an 
approximate ratio of true values by
+         * tracking the counts of true and false values it has generated. It 
will produce runs of consecutive true
+         * values up to the specified maximum length to create a more 
realistic pattern than
+         * independent random choices.
+         *
+         * @param ratio the target ratio of true values (between 0 and 1)
+         * @param maxRuns the maximum length of consecutive true values in a 
run
+         * @return a generator that produces boolean values matching the 
specified distribution pattern
+         * @throws IllegalArgumentException if ratio is not between 0 and 1
+         */
         public Gen<Boolean> biasedRepeatingRuns(double ratio, int maxRuns)
         {
             Invariants.requireArgument(ratio > 0 && ratio <= 1, "Expected %d 
to be larger than 0 and <= 1", ratio);
             double lower = ratio * .8;
             double upper = ratio * 1.2;
-            return new Gen<Boolean>() {
+            return new Gen<>()
+            {
                 // run represents how many consecutaive true values should be 
returned; -1 implies no active "run" exists
                 private int run = -1;
                 private long falseCount = 0, trueCount = 0;
+
                 @Override
                 public Boolean next(RandomSource rs)
                 {
@@ -705,7 +1081,7 @@ public class Gens {
                 switch (selection)
                 {
                     case 0: // uniform 50/50
-                        return r -> r.nextBoolean();
+                        return RandomSource::nextBoolean;
                     case 1: // variable frequency
                         var freq = rs.nextFloat();
                         return r -> r.decide(freq);
@@ -745,11 +1121,18 @@ public class Gens {
             return r -> r.nextInt(min, max + 1);
         }
 
+
+        /**
+         * @see Gens#mixedDistribution(int, int)
+         */
         public Gen<Gen.IntGen> mixedDistribution(int minInclusive, int 
maxExclusive)
         {
             return Gens.mixedDistribution(minInclusive, maxExclusive);
         }
 
+        /**
+         * @see Gens#mixedDistribution(int, int, int)
+         */
         public Gen<Gen.IntGen> mixedDistribution(int minInclusive, int 
maxExclusive, int numBuckets)
         {
             return Gens.mixedDistribution(minInclusive, maxExclusive, 
numBuckets);
@@ -785,11 +1168,21 @@ public class Gens {
             return pick(klass.getEnumConstants());
         }
 
+        /**
+         * A mixedDistribution using the enum classes values as input to 
{@link Gens#mixedDistribution(Object[])}
+         *
+         * @see Gens#mixedDistribution(Object[])
+         */
         public <T extends Enum<T>> Gen<Gen<T>> allMixedDistribution(Class<T> 
klass)
         {
             return mixedDistribution(klass.getEnumConstants());
         }
 
+        /**
+         * Creates a generator that randomly selects one value from the enum 
values based off a set of weights
+         *
+         * @see #pick(Map)
+         */
         public <T extends Enum<T>> Gen<T> allWithWeights(Class<T> klass, 
int... weights)
         {
             T[] constants = klass.getEnumConstants();
@@ -925,11 +1318,13 @@ public class Gens {
         }
     }
 
-    public static class ArrayDSL<T> implements BaseSequenceDSL<ArrayDSL<T>, 
T[]> {
+    public static class ArrayDSL<T> implements BaseSequenceDSL<ArrayDSL<T>, 
T[]>
+    {
         private final Class<T> type;
         private final Gen<T> fn;
 
-        public ArrayDSL(Class<T> type, Gen<T> fn) {
+        public ArrayDSL(Class<T> type, Gen<T> fn)
+        {
             this.type = Objects.requireNonNull(type);
             this.fn = Objects.requireNonNull(fn);
         }
@@ -952,6 +1347,7 @@ public class Gens {
             {
                 Reset.tryReset(fn);
                 int size = sizeGen.nextInt(r);
+                //noinspection unchecked
                 T[] list = (T[]) Array.newInstance(type, size);
                 for (int i = 0; i < size; i++)
                     list[i] = fn.next(r);
@@ -1075,6 +1471,7 @@ public class Gens {
             {
                 T value = null;
                 int i;
+                //noinspection StatementWithEmptyBody
                 for (i = 0; i < 42 && !seen.add((value = fn.next(random))); 
i++) {}
                 if (i == 42) throw IgnoreGenResult.INSTANCE;
                 return value;
diff --git a/accord-core/src/test/java/accord/utils/Property.java 
b/accord-core/src/test/java/accord/utils/Property.java
index eed829ca..92faad08 100644
--- a/accord-core/src/test/java/accord/utils/Property.java
+++ b/accord-core/src/test/java/accord/utils/Property.java
@@ -45,6 +45,8 @@ public class Property
 {
     public static abstract class Common<T extends Common<T>>
     {
+        private static final StackTraceElement[] STACK_TRACE = new 
StackTraceElement[0];
+
         protected long seed = SeedProvider.instance.nextSeed();
         protected int examples = 1000;
 
@@ -62,12 +64,14 @@ public class Property
             this.timeout = other.timeout;
         }
 
+        @SuppressWarnings("unchecked")
         public T withSeed(long seed)
         {
             this.seed = seed;
             return (T) this;
         }
 
+        @SuppressWarnings("unchecked")
         public T withExamples(int examples)
         {
             if (examples <= 0)
@@ -76,12 +80,14 @@ public class Property
             return (T) this;
         }
 
+        @SuppressWarnings("unchecked")
         public T withPure(boolean pure)
         {
             this.pure = pure;
             return (T) this;
         }
 
+        @SuppressWarnings("unchecked")
         public T withTimeout(Duration timeout)
         {
             this.timeout = timeout;
@@ -91,6 +97,7 @@ public class Property
 
         protected void checkWithTimeout(Runnable fn)
         {
+            Invariants.nonNull(timeout);
             try
             {
                 TimeoutUtils.runBlocking(timeout, "property with timeout", 
fn::run);
@@ -106,7 +113,7 @@ public class Property
             catch (TimeoutException e)
             {
                 TimeoutException override = new TimeoutException("property 
test did not complete within " + this.timeout);
-                override.setStackTrace(new StackTraceElement[0]);
+                override.setStackTrace(STACK_TRACE);
                 throw new PropertyError(propertyError(this, override));
             }
         }
@@ -405,11 +412,51 @@ public class Property
         }
     }
 
+    /**
+     * QuickCheck style testing framework for property based testing
+     * <p>
+     * Examples
+     * <pre>
+     * import static accord.utils.Property.qt;
+     *
+     * // Run a simple property test
+     * qt().check(random -> {
+     *     // Test with random values
+     *     int value = random.nextInt();
+     *     assert someInvariant(value);
+     * });
+     * </pre>
+     * <pre>
+     * import static accord.utils.Property.qt;
+     *
+     * // Run a property test with strings as input
+     * qt().forAll(Gens.strings().ofLengthBetween(0, 100))
+     *     .check(str -> {
+     *         // Property assertion goes here
+     *     });
+     * </pre>
+     */
     public static ForBuilder qt()
     {
         return new ForBuilder();
     }
 
+    /**
+     * QuickCheck style testing framework for stateful property testing
+     * <p>
+     * Examples
+     * <pre>
+     * import static accord.utils.Property.stateful;
+     * import static accord.utils.Property.commands;
+     *
+     * stateful().check(commands(() -> State::new)
+     *                 .add(create()) // add "create" command with random 
weight
+     *                 .add(read())   // add "read" command with random weight
+     *                 .add(update()) // add "update" command with random 
weight
+     *                 .add(delete()) // add "delete" command with random 
weight
+     *         .build());
+     * </pre>
+     */
     public static StatefulBuilder stateful()
     {
         return new StatefulBuilder();
@@ -438,7 +485,7 @@ public class Property
             return this;
         }
 
-        @SuppressWarnings("rawtypes")
+        @SuppressWarnings({ "rawtypes", "unchecked" })
         public <State, SystemUnderTest> void check(Commands<State, 
SystemUnderTest> commands)
         {
             RandomSource rs = new DefaultRandom(seed);
@@ -530,6 +577,7 @@ public class Property
             return newHistory;
         }
 
+        @SuppressWarnings({ "rawtypes", "unchecked" })
         private <State, SystemUnderTest> void process(Command cmd, State 
state, SystemUnderTest sut, int id, @Nullable LongArrayList stepTiming) throws 
Throwable
         {
             if (stepTimeout == null)
@@ -544,7 +592,8 @@ public class Property
             }
             finally
             {
-                stepTiming.add(System.nanoTime() - startNanos);
+                if (stepTiming != null)
+                    stepTiming.add(System.nanoTime() - startNanos);
             }
         }
     }
@@ -617,11 +666,13 @@ public class Property
         }
     }
 
+    @SafeVarargs
     public static <State, SystemUnderTest> MultistepCommand<State, 
SystemUnderTest> multistep(Command<State, SystemUnderTest, ?>... cmds)
     {
         return multistep(Arrays.asList(cmds));
     }
 
+    @SuppressWarnings("unchecked")
     public static <State, SystemUnderTest> MultistepCommand<State, 
SystemUnderTest> multistep(List<Command<State, SystemUnderTest, ?>> cmds)
     {
         List<Command<State, SystemUnderTest, ?>> result = new 
ArrayList<>(cmds.size());
@@ -633,6 +684,7 @@ public class Property
         return result::iterator;
     }
 
+    @SuppressWarnings("unchecked")
     private static <State, SystemUnderTest> Collection<? extends 
Command<State, SystemUnderTest, ?>> flatten(MultistepCommand<State, 
SystemUnderTest> mc)
     {
         List<Command<State, SystemUnderTest, ?>> result = new ArrayList<>();
@@ -644,6 +696,7 @@ public class Property
         return result;
     }
 
+    @SuppressWarnings("RedundantThrows")
     public interface MultistepCommand<State, SystemUnderTest> extends 
Command<State, SystemUnderTest, Object>, Iterable<Command<State, 
SystemUnderTest, ?>>
     {
         @Override
@@ -688,6 +741,7 @@ public class Property
         }
     }
 
+    @SuppressWarnings("RedundantThrows")
     public static <State, SystemUnderTest, Result> Command<State, 
SystemUnderTest, Result> ignoreCommand()
     {
         return new Command<>()
@@ -718,6 +772,7 @@ public class Property
         };
     }
 
+    @SuppressWarnings("RedundantThrows")
     public interface UnitCommand<State, SystemUnderTest> extends 
Command<State, SystemUnderTest, Void>
     {
         void applyUnit(State state) throws Throwable;
@@ -736,8 +791,19 @@ public class Property
             runUnit(sut);
             return null;
         }
+
+        @SuppressWarnings("unused")
+        default void checkPostconditions(State state, SystemUnderTest sut) 
throws Throwable {}
+
+        @Override
+        default void checkPostconditions(State state, Void expected,
+                                         SystemUnderTest sut, Void actual) 
throws Throwable
+        {
+            checkPostconditions(state, sut);
+        }
     }
 
+    @SuppressWarnings("RedundantThrows")
     public interface StateOnlyCommand<State> extends UnitCommand<State, Void>
     {
         @Override
@@ -807,6 +873,7 @@ public class Property
         void onFailure(State state, SystemUnderTest sut, List<String> history, 
Throwable cause) throws Throwable;
     }
 
+    @SuppressWarnings("unused")
     public static class CommandsBuilder<State, SystemUnderTest>
     {
         public interface Setup<State, SystemUnderTest>
@@ -1118,13 +1185,13 @@ public class Property
             return new Commands<>()
             {
                 @Override
-                public Gen<State> genInitialState() throws Throwable
+                public Gen<State> genInitialState()
                 {
                     return stateGen.get();
                 }
 
                 @Override
-                public SystemUnderTest createSut(State state) throws Throwable
+                public SystemUnderTest createSut(State state)
                 {
                     return sutFactory.apply(state);
                 }
diff --git a/accord-core/src/test/java/accord/utils/README.md 
b/accord-core/src/test/java/accord/utils/README.md
new file mode 100644
index 00000000..71d0f4a8
--- /dev/null
+++ b/accord-core/src/test/java/accord/utils/README.md
@@ -0,0 +1,318 @@
+# Property Testing
+
+There are multiple ways to do property testing, and this package provides 
different utilities to aid in writing tests.
+
+# Gen
+
+The `Gen` class is the core abstraction for generating random test values in 
our property-based testing framework. A `Gen<T>` instance defines how to 
produce random values of type `T` using a provided `RandomSource`.
+
+## Core Concepts
+
+### The `Gen` Interface
+
+`Gen<T>` is a functional interface with a single method:
+
+```java
+T generate(RandomSource random);
+```
+
+This method produces a random value of type `T` using the given random source.
+
+### Using `Gens` Utility Class
+
+The `Gens` class provides a comprehensive set of pre-defined generators for 
common types:
+
+#### Primitive Types
+- `Gens.bools()`: Generates random boolean values based off a pattern
+- `Gens.ints()`, `Gens.longs()`: Generates random integer/long values based 
off a pattern
+- `Gens.enum()`: Generates enum values based off a pattern
+- `Gens.strings()`: Generates strings based off a pattern
+
+#### Collection Generators
+- `Gens.lists(itemGen)`, `Gens.arrays(class, itemGen)`: Lists/arrays of random 
items based off a pattern
+
+#### Advanced Generators
+- `Gens.oneOf(T... values)`: Selects randomly from provided values
+- `Gens.constant(value)`: Always generates the same value
+
+#### Meta-Randomness Generators
+
+To put it simply: a generator of generators.  These generators define how to 
generate randomness itself!
+
+- `Gens.mixedDistribution`: This is a group of functions that will select 
values from some range or input, and the selection will be based off a 
distribution.  When working with these functions the top level `Gen` selects 
what distribution to use, and will return a `Gen` that selects values from that 
distribution.
+
+### Composing Generators
+
+Generators can be transformed and combined using methods like:
+
+```java
+// Map a generator to a different type
+Gen<String> stringGen = Gens.ints().between(0, 10).map(i -> "Number: " + i);
+
+// Filter generated values
+Gen<Integer> evenNumbers = Gens.ints().between(0, 10).filter(i -> i % 2 == 0);
+```
+
+### Example Usage
+
+```java
+// Generate a random person object
+Gen<String> nameGen = Gens.strings().all().ofLengthBetween(1, 10);
+Gen.IntGen ageGen = Gens.ints().between(18, 100) // IntGen exposes a "nextInt" 
function without boxing
+Gen<Person> personGen = rs -> new Person(nameGen.next(rs), ageGen.nextInt(rs));
+
+// Create a more complex generator with bias
+Gen<Person> complexPersonGen = Gens.oneOf(Map.of(
+    youngPersonGen, 3, // 3x weight for young people
+    adultPersonGen, 2, // 2x weight for adults
+    seniorPersonGen, 1 // 1x weight for seniors
+));
+
+enum Level {IC1, IC2, IC3, IC4, IC5, IC6, IC7}
+// Use "meta-randomness" to have your test change the distribution
+Gen<Gen<Level> levelDistribution = 
Gens.enums().allMixedDistribution(Level.class);
+// Select the actual distribution for the test
+Gen<Level> levelGen = levelDistribution.next(rs);
+```
+
+# Property-Based Testing with `qt()`
+
+Property-based testing generates random test inputs to validate that certain 
properties or invariants of your code hold true regardless of the input values. 
The `Property.qt()` method enables this style of testing, similar to libraries 
like QuickCheck or QuickTheory.
+
+## Basic Usage
+
+```java
+import static accord.utils.Property.qt;
+
+// Run a simple property test
+qt().check(random -> {
+    // Test with random values
+    int value = random.nextInt();
+    assert someInvariant(value);
+});
+```
+
+```java
+import static accord.utils.Property.qt;
+
+// Run a property test with strings as input
+qt().forAll(Gens.strings().ofLengthBetween(0, 100))
+    .check(str -> {
+        // Property assertion goes here
+    });
+```
+
+## Terms
+
+- `seed` - input to a `RandomSource` for reproducible tests
+- `example` - a single test execution
+- `pure` - does the test have side effects (that might impact the 
reproducibility)?
+
+## Main Features
+
+### Core Method: `qt()`
+Returns a `ForBuilder` instance that serves as the starting point for defining 
a property test.
+
+### Configuration Methods
+
+- `withSeed(long seed)`: Sets a specific random seed for the test (useful when 
retrying failed tests)
+- `withExamples(int count)`: Sets the number of test cases to generate 
(default is 1000)
+- `withPure(boolean pure)`: Controls whether to use a fresh random seed for 
each example (default is true)
+- `withTimeout(Duration timeout)`: Sets a timeout for test execution
+
+### Test Input Generation
+
+- `forAll(Gen<T> gen)`: Single generator for one parameter
+- `forAll(Gen<A> a, Gen<B> b)`: Two generators for two parameters
+- `forAll(Gen<A> a, Gen<B> b, Gen<C> c)`: Three generators for three parameters
+
+### Execution
+
+- `check(Consumer<T> fn)`: Runs the test with the specified property function
+
+## Error Handling
+
+When a property test fails, the framework creates a detailed error report 
including:
+
+- The random seed used (for test reproducibility)
+- Number of examples planned
+- Whether pure mode was enabled
+- The error message and exception
+- The generated values that caused the failure
+
+# Stateful Property-Based Testing with `stateful()`
+
+Stateful property testing allows you to verify that your system behaves 
correctly through a sequence of operations. Unlike simple property testing 
which validates individual functions, stateful testing models interactions with 
a stateful system and verifies that invariants hold throughout operation 
sequences.
+
+## Basic Usage
+
+```java
+import static accord.utils.Property.stateful;
+import static accord.utils.Property.commands;
+
+stateful().check(commands(() -> State::new)
+               .add(create()) // add "create" command with random weight
+               .add(read())   // add "read" command with random weight
+               .add(update()) // add "update" command with random weight
+               .add(delete()) // add "delete" command with random weight
+        .build());
+```
+
+## Terms
+
+`stateful` inherits same terms from `qt`, but adds the following
+
+- `State` - state needed by the test to track changes (eg. a model), and/or 
utility functions
+- `SystemUnderTest` - the thing that is being tested
+- `command` - a single operation to apply to both the `State` and the 
`SystemUnderTest`
+- `step` - one `Command` to execute
+
+For some tests, `SystemUnderTest` is not needed, in which case `State` acts as 
both `State` and `SystemUnderTest`.
+
+## Basic Concepts
+
+### Core Method: `stateful()`
+
+The `Property.stateful()` method is the entry point for stateful property 
testing, similar to `qt()` for regular property testing, but takes a `Commands` 
rather than a lambda; this `Commands` defines what is allowed to happen in the 
test.
+
+## Configuration Options
+
+The same configuration options exist as `qt`, but also add the following
+
+- `withSteps(int)`: Maximum number of commands to try per test run (default is 
1000)
+- `withStepTimeout(Duration)`: - Max amount of time a single `step` can take.  
This method differs from `withTimeout` as it focuses on a single `step` where 
as `withTimeout` focuses on the `example`.
+
+### Command
+
+Command consist of three parts:
+1. **Execution**: Apply the `Command` to both the `State` and `SystemUnderTest`
+2. **Verification**: Check that `State` and `SystemUnderTest` maintain a given 
set of properties
+3. **Display**: Creates a human readable string about what this Command does
+
+A typical command looks like this:
+
+```java
+class AddItemCommand implements UnitCommand<ShoppingCart, ActualCart> 
+{
+    private final Item item;
+    
+    public AddItemCommand(Item item) 
+    {
+        this.item = item;
+    }
+    
+    // Update the model (what we expect to happen)
+    @Override
+    public void applyUnit(ShoppingCart model) 
+    {
+        model.add(item);
+    }
+    
+    // Apply to the real system
+    @Override
+    public void runUnit(ActualCart sut) 
+    {
+        sut.addItem(item);
+    }
+    
+    // Verify that model and sut match after command execution
+    @Override
+    public void checkPostconditions(ShoppingCart model, ActualCart sut) 
+    {
+        assertEquals(model.getItems(), sut.getItems());
+    }
+    
+    // For history logging
+    @Override
+    public String detailed(ShoppingCart model) 
+    {
+        return "Add item: " + item;
+    }
+}
+```
+
+But it is common that the test doesn't include a `SystemUnderTest`, in which 
case the command would look like
+
+```java
+class AddItemCommand implements StateOnlyCommand<ShoppingCart>
+{
+    private final Item item;
+    
+    // Constructor for the command with its parameters
+    public AddItemCommand(Item item) 
+    {
+        this.item = item;
+    }
+    
+    // Update the model (what we expect to happen)
+    @Override
+    public void applyUnit(ShoppingCart model) 
+    {
+        model.add(item);
+        // verify anything needed
+    }
+
+    // For history logging
+    @Override
+    public String detailed(ShoppingCart model) 
+    {
+        return "Add item: " + item;
+    }
+}
+```
+
+Or can use `SimpleCommand` instead
+
+```java
+Item item = ...
+new SimpleCommand<>("Add item: " + item, state -> state.add(item));
+```
+
+### Commands
+
+`Commands` represent a set of `Command` that can be performed on both the 
`State` and `SystemUnderTest`.
+
+- `genInitialState` - called at the start of each "example", and creates a new 
`State`
+- `createSut(state)` - called at the start of each "example", and creates a 
new `SystemUnderTest` from the provided `State`
+- `destroyState(state, @Nullable cause)`, `destroySut(sut, @Nullable cause)` - 
called at the end of each "example" to allow closing resources
+- `commands(state)` - called at each "attempt" to create a generator of 
`Command`.
+
+## Commands Builder
+
+The framework provides a `Commands` builder to simplify `Commands` generation:
+
+```java
+commands(() -> State::new, Sut::new)
+// Add command with fixed weight
+.add(3, (rs, state) -> new AddItemCommand(Gens.items().next()))
+// Add command with random weights! At the start of each example, the weight 
is computed; each example will run with different weights
+// Should default to this method over explicit weights
+.add((rs, state) -> new RemoveItemCommand())
+// Add conditional command
+.addIf(state -> !state.isEmpty(),
+          (rs, state) -> new CheckoutCommand())
+.build();
+```
+
+## Error Reporting
+
+When a stateful test fails, the framework provides detailed information:
+- The command sequence that led to the failure
+- The random seed for reproducibility
+- The specific command that failed
+- The exception and error message
+
+## Example: Testing a Queue Implementation
+
+```java
+stateful()
+    .check(commands(() -> MyQueueImplementation::<>new)
+        .add((rs, state) -> new EnqueueCommand<>(Gens.ints().all().next(rs)))
+        .addIf(state -> !state.isEmpty(),
+              (rs, state) -> new DequeueCommand<>())
+        .addIf(state -> !state.isEmpty(),
+              (rs, state) -> new PeekCommand<>())
+        .build());
+```
+
+This will generate and run sequences of queue operations, automatically 
checking that your implementation behaves correctly.


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscr...@cassandra.apache.org
For additional commands, e-mail: commits-h...@cassandra.apache.org

Reply via email to