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]