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.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 2c52a8be6f Improve CQLTester to make it trivial to run the tests with 
different configs, and to add randomness to the test
2c52a8be6f is described below

commit 2c52a8be6fdac7305e433b7f28bd70596ef1444a
Author: David Capwell <[email protected]>
AuthorDate: Tue Aug 20 11:50:34 2024 -0700

    Improve CQLTester to make it trivial to run the tests with different 
configs, and to add randomness to the test
    
    patch by David Capwell; reviewed by Caleb Rackliffe for CASSANDRA-19833
---
 .../cassandra/config/DatabaseDescriptor.java       |  73 ++++++--
 .../cassandra/config/YamlConfigurationLoader.java  |  21 +++
 .../test/sai/VectorDistributedTest.java            |  31 +++-
 test/unit/accord/utils/RandomSource.java           |   2 +-
 test/unit/org/apache/cassandra/cql3/CQLTester.java | 109 +++++++++++
 .../cql3/validation/operations/InsertTest.java     |   8 +-
 .../org/apache/cassandra/index/sai/SAITester.java  |  43 +++--
 .../index/sai/memory/VectorMemoryIndexTest.java    |   5 +
 .../cassandra/utils/AbstractTypeGenerators.java    |  22 +++
 .../cassandra/utils/CassandraGenerators.java       |  68 ++++++-
 .../apache/cassandra/utils/ConfigGenBuilder.java   | 200 +++++++++++++++++++++
 .../cassandra/utils/ConfigGenBuilderTest.java      |  79 ++++++++
 12 files changed, 613 insertions(+), 48 deletions(-)

diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java 
b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
index 35f9055c70..66e0fd0532 100644
--- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
+++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
@@ -50,6 +50,8 @@ import java.util.function.Supplier;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import javax.annotation.Nullable;
+import javax.management.MalformedObjectNameException;
+import javax.management.ObjectName;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
@@ -113,6 +115,7 @@ import org.apache.cassandra.service.CacheService.CacheType;
 import org.apache.cassandra.service.StorageService;
 import org.apache.cassandra.service.paxos.Paxos;
 import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.utils.MBeanWrapper;
 import org.apache.cassandra.utils.StorageCompatibilityMode;
 
 import static 
org.apache.cassandra.config.CassandraRelevantProperties.ALLOCATE_TOKENS_FOR_KEYSPACE;
@@ -256,10 +259,8 @@ public class DatabaseDescriptor
 
     public static void daemonInitialization(Supplier<Config> config) throws 
ConfigurationException
     {
-        if (toolInitialized)
-            throw new AssertionError("toolInitialization() already called");
-        if (clientInitialized)
-            throw new AssertionError("clientInitialization() already called");
+        assertNotToolInitialized();
+        assertNotClientInitialized();
 
         // Some unit tests require this :(
         if (daemonInitialized)
@@ -271,6 +272,40 @@ public class DatabaseDescriptor
         AuthConfig.applyAuth();
     }
 
+    public static void unsafeDaemonInitialization(Supplier<Config> config) 
throws ConfigurationException
+    {
+        assertNotToolInitialized();
+        assertNotClientInitialized();
+
+        daemonInitialized = true;
+
+        setConfig(config.get());
+        clear();
+        applyAll();
+        AuthConfig.applyAuth();
+    }
+
+    private static void clear()
+    {
+        sstableFormats = null;
+        clearMBean("org.apache.cassandra.db:type=DynamicEndpointSnitch");
+        clearMBean("org.apache.cassandra.db:type=EndpointSnitchInfo");
+    }
+
+    private static void clearMBean(String name)
+    {
+        try
+        {
+            ObjectName mbeanName = new ObjectName(name);
+            if (MBeanWrapper.instance.isRegistered(mbeanName))
+                MBeanWrapper.instance.unregisterMBean(mbeanName);
+        }
+        catch (MalformedObjectNameException e)
+        {
+            throw new AssertionError(e);
+        }
+    }
+
     /**
      * Equivalent to {@link #toolInitialization(boolean) 
toolInitialization(true)}.
      */
@@ -295,10 +330,8 @@ public class DatabaseDescriptor
         }
         else
         {
-            if (daemonInitialized)
-                throw new AssertionError("daemonInitialization() already 
called");
-            if (clientInitialized)
-                throw new AssertionError("clientInitialization() already 
called");
+            assertNotDaemonInitialized();
+            assertNotClientInitialized();
         }
 
         if (toolInitialized)
@@ -352,10 +385,8 @@ public class DatabaseDescriptor
         }
         else
         {
-            if (daemonInitialized)
-                throw new AssertionError("daemonInitialization() already 
called");
-            if (toolInitialized)
-                throw new AssertionError("toolInitialization() already 
called");
+            assertNotDaemonInitialized();
+            assertNotToolInitialized();
         }
 
         if (clientInitialized)
@@ -369,6 +400,24 @@ public class DatabaseDescriptor
         applySSTableFormats();
     }
 
+    private static void assertNotDaemonInitialized()
+    {
+        if (daemonInitialized)
+            throw new AssertionError("daemonInitialization() already called");
+    }
+
+    private static void assertNotClientInitialized()
+    {
+        if (clientInitialized)
+            throw new AssertionError("clientInitialization() already called");
+    }
+
+    private static void assertNotToolInitialized()
+    {
+        if (toolInitialized)
+            throw new AssertionError("toolInitialization() already called");
+    }
+
     public static boolean isClientInitialized()
     {
         return clientInitialized;
diff --git a/src/java/org/apache/cassandra/config/YamlConfigurationLoader.java 
b/src/java/org/apache/cassandra/config/YamlConfigurationLoader.java
index a312aa47dd..9bf4e41559 100644
--- a/src/java/org/apache/cassandra/config/YamlConfigurationLoader.java
+++ b/src/java/org/apache/cassandra/config/YamlConfigurationLoader.java
@@ -41,6 +41,7 @@ import org.slf4j.LoggerFactory;
 
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.io.util.File;
+import org.yaml.snakeyaml.DumperOptions;
 import org.yaml.snakeyaml.LoaderOptions;
 import org.yaml.snakeyaml.TypeDescription;
 import org.yaml.snakeyaml.Yaml;
@@ -52,7 +53,9 @@ import org.yaml.snakeyaml.introspector.MissingProperty;
 import org.yaml.snakeyaml.introspector.Property;
 import org.yaml.snakeyaml.introspector.PropertyUtils;
 import org.yaml.snakeyaml.nodes.Node;
+import org.yaml.snakeyaml.nodes.Tag;
 import org.yaml.snakeyaml.parser.ParserImpl;
+import org.yaml.snakeyaml.representer.Representer;
 import org.yaml.snakeyaml.resolver.Resolver;
 
 import static 
org.apache.cassandra.config.CassandraRelevantProperties.ALLOW_DUPLICATE_CONFIG_KEYS;
@@ -258,6 +261,24 @@ public class YamlConfigurationLoader implements 
ConfigurationLoader
         return value;
     }
 
+    public static String toYaml(Object map)
+    {
+        DumperOptions options = new DumperOptions();
+        options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
+        options.setExplicitStart(true);
+        class NoTags extends Representer
+        {
+            public NoTags() {
+                super(options);
+                this.multiRepresenters.put(Enum.class, data -> 
representScalar(Tag.STR, ((Enum<?>) data).name()));
+            }
+        }
+
+        Yaml yaml = new Yaml(new NoTags(), options);
+
+        return yaml.dump(map);
+    }
+
     private static Composer getDefaultComposer(Node node)
     {
         return new Composer(new ParserImpl(null), new Resolver(), 
getDefaultLoaderOptions())
diff --git 
a/test/distributed/org/apache/cassandra/distributed/test/sai/VectorDistributedTest.java
 
b/test/distributed/org/apache/cassandra/distributed/test/sai/VectorDistributedTest.java
index 4c770860bc..c64dbb2ec6 100644
--- 
a/test/distributed/org/apache/cassandra/distributed/test/sai/VectorDistributedTest.java
+++ 
b/test/distributed/org/apache/cassandra/distributed/test/sai/VectorDistributedTest.java
@@ -33,8 +33,12 @@ import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
+import org.junit.FixMethodOrder;
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+import org.junit.runners.MethodSorters;
 
 import io.github.jbellis.jvector.vector.VectorSimilarityFunction;
 import org.apache.cassandra.config.Config;
@@ -49,15 +53,22 @@ import org.apache.cassandra.index.sai.SAITester;
 import org.apache.cassandra.index.sai.disk.v1.IndexWriterConfig;
 import org.apache.cassandra.index.sai.utils.Glove;
 
+import static 
org.apache.cassandra.config.CassandraRelevantProperties.TEST_RANDOM_SEED;
 import static org.apache.cassandra.distributed.api.Feature.GOSSIP;
 import static org.apache.cassandra.distributed.api.Feature.NETWORK;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
+/**
+ * This class relies on a static random source so needs to control the test 
order to make sure any failures could be
+ * reproduced.  This means that if an error is detected then running a single 
test is not enough to reproduce,
+ * you must run the whole class...
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
 public class VectorDistributedTest extends TestBaseImpl
 {
     @Rule
-    public SAITester.FailureWatcher failureRule = new 
SAITester.FailureWatcher();
+    public FailureWatcher failureRule = new FailureWatcher();
 
     private static final String CREATE_KEYSPACE = "CREATE KEYSPACE %%s WITH 
replication = {'class': 'SimpleStrategy', 'replication_factor': %d}";
     private static final String CREATE_TABLE = "CREATE TABLE %%s (pk int 
primary key, val vector<float, %d>)";
@@ -436,4 +447,22 @@ public class VectorDistributedTest extends TestBaseImpl
     {
         return String.format(query, KEYSPACE + '.' + table);
     }
+
+    public static class FailureWatcher extends TestWatcher
+    {
+        @Override
+        protected void failed(Throwable e, Description description)
+        {
+            SAITester.Randomization rand = SAITester.getRandomOrNull();
+            if (rand == null) return;
+
+            String seedProp = TEST_RANDOM_SEED.getKey();
+            StringBuilder sb = new StringBuilder();
+            sb.append("Property error detected:");
+            sb.append("\nSeed: ").append(rand.seed()).append(" -- To rerun do 
-D").append(seedProp).append('=').append(rand.seed());
+            String message = e.toString();
+            sb.append("\nError:\n\t").append(message.replaceAll("\n", "\n\t"));
+            throw new AssertionError(sb.toString(), e);
+        }
+    }
 }
diff --git a/test/unit/accord/utils/RandomSource.java 
b/test/unit/accord/utils/RandomSource.java
index 3d4861e5e4..4e60d7f805 100644
--- a/test/unit/accord/utils/RandomSource.java
+++ b/test/unit/accord/utils/RandomSource.java
@@ -301,7 +301,7 @@ public interface RandomSource
 
     default Random asJdkRandom()
     {
-        return new Random()
+        return new Random(nextLong())
         {
             @Override
             public void setSeed(long seed)
diff --git a/test/unit/org/apache/cassandra/cql3/CQLTester.java 
b/test/unit/org/apache/cassandra/cql3/CQLTester.java
index 00cee8e573..236480cea5 100644
--- a/test/unit/org/apache/cassandra/cql3/CQLTester.java
+++ b/test/unit/org/apache/cassandra/cql3/CQLTester.java
@@ -76,9 +76,15 @@ import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Rule;
 import org.junit.rules.TestName;
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import accord.utils.DefaultRandom;
+import accord.utils.Gen;
+import accord.utils.Property;
+import accord.utils.RandomSource;
 import com.codahale.metrics.Gauge;
 import com.datastax.driver.core.CloseFuture;
 import com.datastax.driver.core.Cluster;
@@ -106,9 +112,11 @@ import org.apache.cassandra.auth.IAuthenticator;
 import org.apache.cassandra.auth.IRoleManager;
 import org.apache.cassandra.concurrent.Stage;
 import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.config.Config;
 import org.apache.cassandra.config.DataStorageSpec;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.EncryptionOptions;
+import org.apache.cassandra.config.YamlConfigurationLoader;
 import org.apache.cassandra.cql3.functions.FunctionName;
 import org.apache.cassandra.cql3.functions.types.ParseUtils;
 import org.apache.cassandra.db.ColumnFamilyStore;
@@ -174,6 +182,7 @@ import org.apache.cassandra.transport.SimpleClient;
 import org.apache.cassandra.transport.TlsTestUtils;
 import org.apache.cassandra.transport.messages.ResultMessage;
 import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.ConfigGenBuilder;
 import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.JMXServerUtils;
 import org.apache.cassandra.utils.Pair;
@@ -184,6 +193,7 @@ import org.awaitility.Awaitility;
 import static 
org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_JMX_LOCAL_PORT;
 import static 
org.apache.cassandra.config.CassandraRelevantProperties.TEST_DRIVER_CONNECTION_TIMEOUT_MS;
 import static 
org.apache.cassandra.config.CassandraRelevantProperties.TEST_DRIVER_READ_TIMEOUT_MS;
+import static 
org.apache.cassandra.config.CassandraRelevantProperties.TEST_RANDOM_SEED;
 import static 
org.apache.cassandra.config.CassandraRelevantProperties.TEST_REUSE_PREPARED;
 import static 
org.apache.cassandra.config.CassandraRelevantProperties.TEST_ROW_CACHE_SIZE;
 import static 
org.apache.cassandra.config.CassandraRelevantProperties.TEST_USE_PREPARED;
@@ -2990,6 +3000,105 @@ public abstract class CQLTester
         }
     }
 
+    /**
+     * Enhances {@link CQLTester} to make it easier for tests to leverage 
randomness.  This class is not the best way to
+     * leverage randomness as it won't run the tests against multiple seeds 
(so could take a long time to find faults)
+     *
+     * The main use case for this class is to take existing tests or tests 
patterns people wish to write, and make it easy
+     * and safe to add randomness.  One main advantage is that the node spun 
up has non-static configs, meaning that each
+     * test run can explore different spaces and avoid having to create a new 
yaml/CI pipeline to test different configs.
+     *
+     * When possible {@link Property#qt()} should be leveraged as it will 
rerun the test many times with different seeds.
+     */
+    public static abstract class Fuzzed extends CQLTester
+    {
+        private static RandomSource RANDOM;
+        private static long SEED;
+        private static String CONFIG = null;
+        protected static Gen<Map<String, Object>> CONFIG_GEN = null;
+
+        @Rule
+        public FailureWatcher failureRule = new FailureWatcher();
+
+        @BeforeClass
+        public static void setUpClass()
+        {
+            setupSeed();
+            try
+            {
+                updateConfigs();
+                prePrepareServer();
+
+                // Once per-JVM is enough
+                prepareServer();
+            }
+            catch (Throwable t)
+            {
+                throwPropertyError(t);
+            }
+        }
+
+        protected static RandomSource random()
+        {
+            if (RANDOM == null)
+                setupSeed();
+            return RANDOM;
+        }
+
+        protected static long seed()
+        {
+            return SEED;
+        }
+
+        protected static void setupSeed()
+        {
+            if (RANDOM != null) return;
+            SEED = TEST_RANDOM_SEED.getLong(new DefaultRandom().nextLong());
+            RANDOM = new DefaultRandom(SEED);
+        }
+
+        @Before
+        public void resetSeed()
+        {
+            RANDOM.setSeed(SEED);
+        }
+
+        protected static void updateConfigs()
+        {
+            if (CONFIG_GEN == null)
+                CONFIG_GEN = new ConfigGenBuilder().build();
+            Map<String, Object> config = CONFIG_GEN.next(RANDOM);
+            CONFIG = YamlConfigurationLoader.toYaml(config);
+
+            Config c = 
ConfigGenBuilder.santize(DatabaseDescriptor.loadConfig());
+            YamlConfigurationLoader.updateFromMap(config, true, c);
+
+            DatabaseDescriptor.unsafeDaemonInitialization(() -> c);
+        }
+
+        public static class FailureWatcher extends TestWatcher
+        {
+            @Override
+            protected void failed(Throwable e, Description description)
+            {
+                throwPropertyError(e);
+            }
+        }
+
+        private static AssertionError throwPropertyError(Throwable e)
+        {
+            String seedProp = TEST_RANDOM_SEED.getKey();
+            StringBuilder sb = new StringBuilder();
+            sb.append("Property error detected:");
+            sb.append("\nSeed: ").append(SEED).append(" -- To rerun do 
-D").append(seedProp).append('=').append(SEED);
+            if (CONFIG != null)
+                sb.append("\nConfig:\n\t").append(CONFIG.replaceAll("\n", 
"\n\t"));
+            String message = e.toString();
+            sb.append("\nError:\n\t").append(message.replaceAll("\n", "\n\t"));
+            throw new AssertionError(sb.toString(), e);
+        }
+    }
+
     public static abstract class InMemory extends CQLTester
     {
         protected static ListenableFileSystem fs = null;
diff --git 
a/test/unit/org/apache/cassandra/cql3/validation/operations/InsertTest.java 
b/test/unit/org/apache/cassandra/cql3/validation/operations/InsertTest.java
index f6e71ae3a5..70b8d3e9da 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/operations/InsertTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/operations/InsertTest.java
@@ -30,7 +30,7 @@ import org.apache.cassandra.cql3.UntypedResultSet;
 import org.apache.cassandra.cql3.UntypedResultSet.Row;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 
-public class InsertTest extends CQLTester
+public class InsertTest extends CQLTester.Fuzzed
 {
     @Test
     public void testInsertZeroDuration() throws Throwable
@@ -75,7 +75,7 @@ public class InsertTest extends CQLTester
         createTable("CREATE TABLE %s (k int PRIMARY KEY, v int)");
         execute("INSERT INTO %s (k, v) VALUES (0, 0) USING TTL ?", (Object) 
null);
         execute("INSERT INTO %s (k, v) VALUES (1, 1) USING TTL ?", 
ByteBuffer.wrap(new byte[0]));
-        assertRows(execute("SELECT k, v, ttl(v) FROM %s"), row(1, 1, null), 
row(0, 0, null));
+        assertRowsIgnoringOrder(execute("SELECT k, v, ttl(v) FROM %s"), row(1, 
1, null), row(0, 0, null));
     }
 
     @Test
@@ -231,13 +231,13 @@ public class InsertTest extends CQLTester
         execute("INSERT INTO %s (partitionKey, staticValue) VALUES (1, 'B')");
         flush(forceFlush);
 
-        assertRows(execute("SELECT * FROM %s"),
+        assertRowsIgnoringOrder(execute("SELECT * FROM %s"),
                    row(1, null, null, "B", null),
                    row(0, 0, 0, "A", null));
 
         execute("INSERT INTO %s (partitionKey, clustering_1, clustering_2, 
value) VALUES (1, 0, 0, 0)");
         flush(forceFlush);
-        assertRows(execute("SELECT * FROM %s"),
+        assertRowsIgnoringOrder(execute("SELECT * FROM %s"),
                    row(1, 0, 0, "B", 0),
                    row(0, 0, 0, "A", null));
 
diff --git a/test/unit/org/apache/cassandra/index/sai/SAITester.java 
b/test/unit/org/apache/cassandra/index/sai/SAITester.java
index ffa3d6a75e..e1e6af7f0b 100644
--- a/test/unit/org/apache/cassandra/index/sai/SAITester.java
+++ b/test/unit/org/apache/cassandra/index/sai/SAITester.java
@@ -41,6 +41,7 @@ import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
+import javax.annotation.Nullable;
 import javax.management.AttributeNotFoundException;
 import javax.management.ObjectName;
 
@@ -50,8 +51,6 @@ import org.junit.Assert;
 import org.junit.BeforeClass;
 import org.junit.Rule;
 import org.junit.rules.TestRule;
-import org.junit.rules.TestWatcher;
-import org.junit.runner.Description;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -62,6 +61,8 @@ import com.datastax.driver.core.ResultSet;
 import com.datastax.driver.core.Session;
 import org.apache.cassandra.concurrent.Stage;
 import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.DurationSpec;
 import org.apache.cassandra.cql3.CQLTester;
 import org.apache.cassandra.cql3.ColumnIdentifier;
 import org.apache.cassandra.cql3.UntypedResultSet;
@@ -102,12 +103,12 @@ import org.apache.cassandra.schema.Schema;
 import org.apache.cassandra.schema.TableMetadata;
 import org.apache.cassandra.service.StorageService;
 import org.apache.cassandra.service.snapshot.TableSnapshot;
+import org.apache.cassandra.utils.ConfigGenBuilder;
 import org.apache.cassandra.utils.JVMStabilityInspector;
 import org.apache.cassandra.utils.Throwables;
 import org.apache.cassandra.utils.bytecomparable.ByteComparable;
 import org.apache.lucene.codecs.CodecUtil;
 
-import static 
org.apache.cassandra.config.CassandraRelevantProperties.TEST_RANDOM_SEED;
 import static org.apache.cassandra.inject.ActionBuilder.newActionBuilder;
 import static org.apache.cassandra.inject.Expression.quote;
 import static org.apache.cassandra.inject.InvokePointBuilder.newInvokePoint;
@@ -115,7 +116,7 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
-public abstract class SAITester extends CQLTester
+public abstract class SAITester extends CQLTester.Fuzzed
 {
     protected static final Logger logger = 
LoggerFactory.getLogger(SAITester.class);
 
@@ -155,7 +156,13 @@ public abstract class SAITester extends CQLTester
     @BeforeClass
     public static void setUpClass()
     {
-        CQLTester.setUpClass();
+        CONFIG_GEN = new ConfigGenBuilder()
+                     .withPartitioner(Murmur3Partitioner.instance)
+                     // some tests timeout in CI with batch, so rely only on 
perioid
+                     .withCommitLogSync(Config.CommitLogSync.periodic)
+                     .withCommitLogSyncPeriod(new 
DurationSpec.IntMillisecondsBound(10, TimeUnit.SECONDS))
+                     .build();
+        CQLTester.Fuzzed.setUpClass();
 
         // Ensure that the on-disk format statics are loaded before the test 
run
         Version.LATEST.onDiskFormat();
@@ -164,9 +171,6 @@ public abstract class SAITester extends CQLTester
     @Rule
     public TestRule testRules = new ResourceLeakDetector();
 
-    @Rule
-    public FailureWatcher failureRule = new FailureWatcher();
-
     @After
     public void removeAllInjections()
     {
@@ -183,6 +187,12 @@ public abstract class SAITester extends CQLTester
         return random;
     }
 
+    @Nullable
+    public static Randomization getRandomOrNull()
+    {
+        return random;
+    }
+
     public enum CorruptionType
     {
         REMOVED
@@ -826,18 +836,16 @@ public abstract class SAITester extends CQLTester
 
     public static class Randomization
     {
-        private final long seed;
         private final Random random;
 
         Randomization()
         {
-            seed = TEST_RANDOM_SEED.getLong(System.nanoTime());
-            random = new Random(seed);
+            random = random().asJdkRandom();
         }
 
-        public void printSeedOnFailure()
+        public long seed()
         {
-            logger.error("Randomized test failed. To rerun test use -D{}={}", 
TEST_RANDOM_SEED.getKey(), seed);
+            return Fuzzed.seed();
         }
 
         public int nextInt()
@@ -917,15 +925,6 @@ public abstract class SAITester extends CQLTester
         }
     }
 
-    public static class FailureWatcher extends TestWatcher
-    {
-        @Override
-        protected void failed(Throwable e, Description description)
-        {
-            if (random != null)
-                random.printSeedOnFailure();
-        }
-    }
     /**
      * Run repeated verification task concurrently with target test
      */
diff --git 
a/test/unit/org/apache/cassandra/index/sai/memory/VectorMemoryIndexTest.java 
b/test/unit/org/apache/cassandra/index/sai/memory/VectorMemoryIndexTest.java
index 2fafaf9ce0..9a0ba6177a 100644
--- a/test/unit/org/apache/cassandra/index/sai/memory/VectorMemoryIndexTest.java
+++ b/test/unit/org/apache/cassandra/index/sai/memory/VectorMemoryIndexTest.java
@@ -96,11 +96,16 @@ public class VectorMemoryIndexTest extends SAITester
     @BeforeClass
     public static void setUpClass()
     {
+        setupSeed();
         // Because this test wants to explicitly set tokens for the local 
node, we override SAITester::setUpClass, as
         // that calls CQLTester::setUpClass, which is opinionated about the 
locally owned tokens.
         MEMTABLE_SHARD_COUNT.setInt(8);
         ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION.setBoolean(true);
+
+        updateConfigs();
+
         ServerTestUtils.prepareServerNoRegister();
+        //TODO (repeatability): this uses ThreadLocalRandom which doesn't let 
you control the seed, so this class can not be repeated...
         
ServerTestUtils.registerLocal(BootStrapper.getRandomTokens(ClusterMetadata.current(),
 10));
         ServerTestUtils.markCMS();
         // Ensure that the on-disk format statics are loaded before the test 
run
diff --git a/test/unit/org/apache/cassandra/utils/AbstractTypeGenerators.java 
b/test/unit/org/apache/cassandra/utils/AbstractTypeGenerators.java
index d119da7bed..257847cda6 100644
--- a/test/unit/org/apache/cassandra/utils/AbstractTypeGenerators.java
+++ b/test/unit/org/apache/cassandra/utils/AbstractTypeGenerators.java
@@ -99,6 +99,7 @@ import org.quicktheories.core.RandomnessSource;
 import org.quicktheories.generators.SourceDSL;
 import org.quicktheories.impl.JavaRandom;
 
+import static 
org.apache.cassandra.utils.AbstractTypeGenerators.TypeKind.COUNTER;
 import static org.apache.cassandra.utils.Generators.IDENTIFIER_GEN;
 import static org.apache.cassandra.utils.Generators.filter;
 
@@ -517,6 +518,27 @@ public final class AbstractTypeGenerators
         return new TypeGenBuilder();
     }
 
+    /**
+     * Similar to {@link #typeGen()} but removes types that are known to be 
problematic in some cases and limits the depth
+     * of the type tree to avoid cell constratins.
+     */
+    public static Gen<AbstractType<?>> safeTypeGen()
+    {
+        TypeGenBuilder baseline = AbstractTypeGenerators.builder()
+                                                        // neither of these 
types support the property
+                                                        //   expected == 
fromComparableBytes(asComparableBytes(expected))
+                                                        // so rather than 
having tests try to skip... just avoid those types!
+                                                        .withoutEmpty()
+                                                        
.withoutTypeKinds(COUNTER)
+                                                        
.withoutPrimitive(DecimalType.instance)
+                                                        // its ordering is 
special...
+                                                        
.withoutPrimitive(DurationType.instance);
+        // composite requires all elements fit into Short.MAX_VALUE bytes
+        // so try to limit the possible expansion of types
+        return baseline.withCompositeElementGen(new 
TypeGenBuilder(baseline).withDefaultSizeGen(1).withMaxDepth(1).build())
+                       .build();
+    }
+
     public static Gen<AbstractType<?>> typeGen()
     {
         return typeGen(3);
diff --git a/test/unit/org/apache/cassandra/utils/CassandraGenerators.java 
b/test/unit/org/apache/cassandra/utils/CassandraGenerators.java
index c6ab6b9ac4..da93b264b7 100644
--- a/test/unit/org/apache/cassandra/utils/CassandraGenerators.java
+++ b/test/unit/org/apache/cassandra/utils/CassandraGenerators.java
@@ -53,7 +53,6 @@ import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.marshal.ByteBufferAccessor;
 import org.apache.cassandra.db.marshal.CompositeType;
 import org.apache.cassandra.db.marshal.EmptyType;
-import org.apache.cassandra.db.marshal.TimeUUIDType;
 import org.apache.cassandra.db.rows.Cell;
 import org.apache.cassandra.db.marshal.UserType;
 import org.apache.cassandra.dht.ByteOrderedPartitioner;
@@ -112,12 +111,6 @@ public final class CassandraGenerators
         return InetAddressAndPort.getByAddressOverrideDefaults(address, 
NETWORK_PORT_GEN.generate(rnd));
     };
 
-    private static final Gen<IPartitioner> PARTITIONER_GEN = 
SourceDSL.arbitrary().pick(Murmur3Partitioner.instance,
-                                                                               
         ByteOrderedPartitioner.instance,
-                                                                               
         new LocalPartitioner(TimeUUIDType.instance),
-                                                                               
         OrderPreservingPartitioner.instance,
-                                                                               
         RandomPartitioner.instance);
-
 
     public static final Gen<TableId> TABLE_ID_GEN = 
Generators.UUID_RANDOM_GEN.map(TableId::fromUUID);
     private static final Gen<TableMetadata.Kind> TABLE_KIND_GEN = 
SourceDSL.arbitrary().pick(TableMetadata.Kind.REGULAR, 
TableMetadata.Kind.INDEX, TableMetadata.Kind.VIRTUAL);
@@ -185,6 +178,11 @@ public final class CassandraGenerators
         return new TableMetadataBuilder().withKeyspaceName(ks).build(rnd);
     }
 
+    public static Gen<String> sstableFormatNames()
+    {
+        return SourceDSL.arbitrary().pick("big", "bti");
+    }
+
     public static class TableMetadataBuilder
     {
         private Gen<String> ksNameGen = CassandraGenerators.KEYSPACE_NAME_GEN;
@@ -354,7 +352,7 @@ public final class CassandraGenerators
             if (memtableKeyGen != null)
                 
params.memtable(MemtableParams.get(memtableKeyGen.generate(rnd)));
             TableMetadata.Builder builder = TableMetadata.builder(ks, 
tableName, tableIdGen.generate(rnd))
-                                                         
.partitioner(PARTITIONER_GEN.generate(rnd))
+                                                         
.partitioner(partitioners().generate(rnd))
                                                          
.kind(tableKindGen.generate(rnd))
                                                          
.isCounter(BOOLEAN_GEN.generate(rnd))
                                                          
.params(params.build());
@@ -597,6 +595,21 @@ public final class CassandraGenerators
         return rs -> partitioner.getToken(bytes.generate(rs));
     }
 
+    public static Gen<IPartitioner> localPartitioner()
+    {
+        return AbstractTypeGenerators.safeTypeGen().map(LocalPartitioner::new);
+    }
+
+    public static Gen<Token> localPartitionerToken()
+    {
+        var lpGen = localPartitioner();
+        return rs -> {
+            var lp = lpGen.generate(rs);
+            var bytes = 
AbstractTypeGenerators.getTypeSupport(lp.getTokenValidator()).bytesGen();
+            return lp.getToken(bytes.generate(rs));
+        };
+    }
+
     public static Gen<Token> orderPreservingToken()
     {
         Gen<String> string = Generators.utf8(0, 10);
@@ -610,6 +623,45 @@ public final class CassandraGenerators
         throw new UnsupportedOperationException("Unsupported partitioner: " + 
partitioner.getClass());
     }
 
+    private enum SupportedPartitioners
+    {
+        Murmur(ignore -> Murmur3Partitioner.instance),
+        ByteOrdered(ignore -> ByteOrderedPartitioner.instance),
+        Random(ignore -> RandomPartitioner.instance),
+        Local(localPartitioner()),
+        OrderPreserving(ignore -> OrderPreservingPartitioner.instance);
+
+        private final Gen<IPartitioner> partitioner;
+
+        SupportedPartitioners(Gen<IPartitioner> partitionerGen)
+        {
+            partitioner = partitionerGen;
+        }
+
+        public Gen<IPartitioner> partitioner()
+        {
+            return partitioner;
+        }
+    }
+
+    public static Gen<IPartitioner> partitioners()
+    {
+        return SourceDSL.arbitrary().enumValues(SupportedPartitioners.class)
+                        .flatMap(SupportedPartitioners::partitioner);
+    }
+
+    public static Gen<IPartitioner> nonLocalPartitioners()
+    {
+        return SourceDSL.arbitrary().enumValues(SupportedPartitioners.class)
+                        .assuming(p -> p != SupportedPartitioners.Local)
+                        .flatMap(SupportedPartitioners::partitioner);
+    }
+
+    public static Gen<Token> token()
+    {
+        return partitioners().flatMap(CassandraGenerators::token);
+    }
+
     public static Gen<Token> token(IPartitioner partitioner)
     {
         if (partitioner instanceof Murmur3Partitioner) return murmurToken();
diff --git a/test/unit/org/apache/cassandra/utils/ConfigGenBuilder.java 
b/test/unit/org/apache/cassandra/utils/ConfigGenBuilder.java
new file mode 100644
index 0000000000..6f814f809a
--- /dev/null
+++ b/test/unit/org/apache/cassandra/utils/ConfigGenBuilder.java
@@ -0,0 +1,200 @@
+/*
+ * 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 org.apache.cassandra.utils;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.collect.ImmutableMap;
+
+import accord.utils.Gen;
+import accord.utils.Gens;
+import accord.utils.RandomSource;
+import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.DurationSpec;
+import org.apache.cassandra.dht.IPartitioner;
+
+public class ConfigGenBuilder
+{
+    public enum Memtable
+    {SkipListMemtable, TrieMemtable, ShardedSkipListMemtable}
+
+    Gen<IPartitioner> partitionerGen = 
Generators.toGen(CassandraGenerators.nonLocalPartitioners());
+    Gen<Config.DiskAccessMode> commitLogDiskAccessModeGen = 
Gens.enums().all(Config.DiskAccessMode.class)
+                                                                .filter(m -> m 
!= Config.DiskAccessMode.standard
+                                                                             
&& m != Config.DiskAccessMode.mmap_index_only
+                                                                             
&& m != Config.DiskAccessMode.direct); // don't allow direct as not every 
filesystem supports it, making the config environment specific
+    Gen<Config.DiskAccessMode> diskAccessModeGen = 
Gens.enums().all(Config.DiskAccessMode.class).filter(m -> m != 
Config.DiskAccessMode.direct);
+    Gen<String> sstableFormatGen = 
Generators.toGen(CassandraGenerators.sstableFormatNames());
+    Gen<Config.MemtableAllocationType> memtableAllocationTypeGen = 
Gens.enums().all(Config.MemtableAllocationType.class);
+    Gen<Memtable> memtableGen = Gens.enums().all(Memtable.class);
+    Gen<Config.CommitLogSync> commitLogSyncGen = 
Gens.enums().all(Config.CommitLogSync.class);
+    Gen<DurationSpec.IntMillisecondsBound> commitLogSyncPeriodGen = rs -> {
+        // how long?
+        long periodMillis;
+        switch (rs.nextInt(0, 2))
+        {
+            case 0: // millis
+                periodMillis = rs.nextLong(1, 20);
+                break;
+            case 1: // seconds
+                periodMillis = TimeUnit.SECONDS.toMillis(rs.nextLong(1, 20));
+                break;
+            default:
+                throw new AssertionError();
+        }
+        return new DurationSpec.IntMillisecondsBound(periodMillis);
+    };
+    // group blocks each and every write for X milliseconds which cause tests 
to take a lot of time,
+    // for this reason the period must be "short"
+    Gen<DurationSpec.IntMillisecondsBound> commitlogSyncGroupWindowGen = 
Gens.longs().between(1, 20).map(l -> new DurationSpec.IntMillisecondsBound(l));
+
+    /**
+     * When loading the {@link Config} from a yaml its possible that some 
configs set will conflict with the configs that get generated here, to avoid 
that set them to a good default
+     */
+    public static Config santize(Config config)
+    {
+        Config defaults = new Config();
+        config.commitlog_sync = defaults.commitlog_sync;
+        config.commitlog_sync_group_window = 
defaults.commitlog_sync_group_window;
+        config.commitlog_sync_period = defaults.commitlog_sync_period;
+        return config;
+    }
+
+    public ConfigGenBuilder withPartitioner(IPartitioner instance)
+    {
+        this.partitionerGen = ignore -> instance;
+        return this;
+    }
+
+    public ConfigGenBuilder withCommitLogSync(Config.CommitLogSync 
commitLogSync)
+    {
+        this.commitLogSyncGen = ignore -> commitLogSync;
+        return this;
+    }
+
+    public ConfigGenBuilder 
withCommitLogSyncPeriod(DurationSpec.IntMillisecondsBound value)
+    {
+        Objects.requireNonNull(value);
+        commitLogSyncPeriodGen = ignore -> value;
+        return this;
+    }
+
+    public Gen<Map<String, Object>> build()
+    {
+        return rs -> {
+            Map<String, Object> config = new LinkedHashMap<>();
+
+            updateConfigPartitioner(rs, config);
+            updateConfigCommitLog(rs, config);
+            updateConfigMemtable(rs, config);
+            updateConfigSSTables(rs, config);
+            updateConfigDisk(rs, config);
+            return config;
+        };
+    }
+
+    private void updateConfigPartitioner(RandomSource rs, Map<String, Object> 
config)
+    {
+        IPartitioner partitioner = partitionerGen.next(rs);
+        config.put("partitioner", partitioner.getClass().getSimpleName());
+    }
+
+    private void updateConfigCommitLog(RandomSource rs, Map<String, Object> 
config)
+    {
+        Config.CommitLogSync commitlog_sync = commitLogSyncGen.next(rs);
+        config.put("commitlog_sync", commitlog_sync);
+        switch (commitlog_sync)
+        {
+            case batch:
+                break;
+            case periodic:
+                config.put("commitlog_sync_period", 
commitLogSyncPeriodGen.next(rs).toString());
+                break;
+            case group:
+                config.put("commitlog_sync_group_window", 
commitlogSyncGroupWindowGen.next(rs).toString());
+                break;
+            default:
+                throw new AssertionError(commitlog_sync.name());
+        }
+        config.put("commitlog_disk_access_mode", 
commitLogDiskAccessModeGen.next(rs));
+    }
+
+    private void updateConfigMemtable(RandomSource rs, Map<String, Object> 
config)
+    {
+        config.put("memtable_allocation_type", 
memtableAllocationTypeGen.next(rs));
+        Memtable defaultMemtable = memtableGen.next(rs);
+        Map<String, Map<String, Object>> memtables = new LinkedHashMap<>();
+        if (rs.nextBoolean())
+        {
+            // use inherits
+            for (Memtable m : Memtable.values())
+                memtables.put(m.name(), createConfig(m).next(rs));
+            memtables.put("default", ImmutableMap.of("inherits", 
defaultMemtable.name()));
+        }
+        else
+        {
+            // define inline
+            memtables.put("default", createConfig(defaultMemtable).next(rs));
+        }
+        config.put("memtable", ImmutableMap.of("configurations", memtables));
+    }
+
+    private static Gen<Map<String, Object>> createConfig(Memtable type)
+    {
+        return rs -> {
+            ImmutableMap.Builder<String, Object> builder = 
ImmutableMap.builder();
+            builder.put("class_name", type.name());
+            ImmutableMap.Builder<String, Object> parametersBuilder = 
ImmutableMap.builder();
+            switch (type)
+            {
+                case TrieMemtable:
+                {
+                    if (rs.nextBoolean())
+                        parametersBuilder.put("shards", rs.nextInt(1, 64));
+                }
+                break;
+                case ShardedSkipListMemtable:
+                {
+                    if (rs.nextBoolean())
+                        parametersBuilder.put("serialize_writes", 
rs.nextBoolean());
+                    if (rs.nextBoolean())
+                        parametersBuilder.put("shards", rs.nextInt(1, 64));
+                }
+                break;
+            }
+            ImmutableMap<String, Object> params = parametersBuilder.build();
+            if (!params.isEmpty())
+                builder.put("parameters", params);
+            return builder.build();
+        };
+    }
+
+    private void updateConfigSSTables(RandomSource rs, Map<String, Object> 
config)
+    {
+        config.put("sstable", ImmutableMap.of("selected_format", 
sstableFormatGen.next(rs)));
+    }
+
+    private void updateConfigDisk(RandomSource rs, Map<String, Object> config)
+    {
+        config.put("disk_access_mode", diskAccessModeGen.next(rs));
+    }
+}
diff --git a/test/unit/org/apache/cassandra/utils/ConfigGenBuilderTest.java 
b/test/unit/org/apache/cassandra/utils/ConfigGenBuilderTest.java
new file mode 100644
index 0000000000..99df56c46b
--- /dev/null
+++ b/test/unit/org/apache/cassandra/utils/ConfigGenBuilderTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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 org.apache.cassandra.utils;
+
+import java.util.Map;
+
+import com.google.common.jimfs.Jimfs;
+import org.junit.Test;
+
+import accord.utils.Gen;
+import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.ParameterizedClass;
+import org.apache.cassandra.config.YamlConfigurationLoader;
+import org.apache.cassandra.io.util.File;
+import org.apache.cassandra.locator.SimpleSeedProvider;
+
+import static accord.utils.Property.qt;
+
+public class ConfigGenBuilderTest
+{
+    static
+    {
+        File.unsafeSetFilesystem(Jimfs.newFileSystem("testing"));
+    }
+
+    private static final Gen<Map<String, Object>> GEN = new 
ConfigGenBuilder().build();
+
+    @Test
+    public void validConfigs()
+    {
+        qt().forAll(GEN).check(config -> validate(config, defaultConfig()));
+    }
+
+    @Test
+    public void validConfigsWithDefaults()
+    {
+        qt().forAll(GEN).check(config -> validate(config, simpleConfig()));
+    }
+
+    private static void validate(Map<String, Object> config, Config c)
+    {
+        YamlConfigurationLoader.updateFromMap(config, true, c);
+        DatabaseDescriptor.unsafeDaemonInitialization(() -> c);
+    }
+
+    private static Config defaultConfig()
+    {
+        return ConfigGenBuilder.santize(DatabaseDescriptor.loadConfig());
+    }
+
+    private static Config simpleConfig()
+    {
+        Config c = new Config();
+        c.commitlog_directory = "/commitlog";
+        c.hints_directory = "/hints";
+        c.saved_caches_directory = "/saved_caches_directory";
+        c.data_file_directories = new String[]{"/data_file_directories"};
+        c.endpoint_snitch = "SimpleSnitch";
+        c.seed_provider = new 
ParameterizedClass(SimpleSeedProvider.class.getName());
+        return c;
+    }
+}
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to