From 191ede53a93a3879ddff7334503485e143e1c0bc Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Sun, 23 Aug 2026 23:36:23 -0300 Subject: [PATCH 1/8] Add Cassandra Actions --- .../database/cassandra/CassandraColumn.kt | 25 +++++ .../cassandra/CassandraColumnGeneBuilder.kt | 66 +++++++++++++ .../database/cassandra/CassandraDbAction.kt | 51 ++++++++++ .../cassandra/CassandraDbActionResult.kt | 36 +++++++ .../cassandra/CassandraDbActionTransformer.kt | 39 ++++++++ .../cassandra/CassandraInsertBuilder.kt | 54 +++++++++++ .../cassandra/CassandraLiteralRenderer.kt | 45 +++++++++ .../cassandra/CassandraTableSchemaParser.kt | 97 +++++++++++++++++++ 8 files changed, 413 insertions(+) create mode 100644 core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraColumn.kt create mode 100644 core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraColumnGeneBuilder.kt create mode 100644 core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraDbAction.kt create mode 100644 core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraDbActionResult.kt create mode 100644 core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraDbActionTransformer.kt create mode 100644 core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraInsertBuilder.kt create mode 100644 core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraLiteralRenderer.kt create mode 100644 core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraTableSchemaParser.kt diff --git a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraColumn.kt b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraColumn.kt new file mode 100644 index 0000000000..8f34534ade --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraColumn.kt @@ -0,0 +1,25 @@ +package org.evomaster.core.database.cassandra + +/** + * A single column of a Cassandra table, as recovered from the schema description string carried by + * a failed CQL query reported by the SUT driver. + */ +data class CassandraColumn( + + val name: String, + + /** + * The CQL type of the column, as named in the CQL schema, eg "text", "int", "map". + */ + val cqlType: String, + + /** + * Whether this column is part of the table's partition key. + */ + val isPartitionKey: Boolean = false, + + /** + * Whether this column is one of the table's clustering columns. + */ + val isClusteringColumn: Boolean = false +) diff --git a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraColumnGeneBuilder.kt b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraColumnGeneBuilder.kt new file mode 100644 index 0000000000..b07890fb50 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraColumnGeneBuilder.kt @@ -0,0 +1,66 @@ +package org.evomaster.core.database.cassandra + +import org.evomaster.core.search.gene.BooleanGene +import org.evomaster.core.search.gene.Gene +import org.evomaster.core.search.gene.UUIDGene +import org.evomaster.core.search.gene.datetime.DateGene +import org.evomaster.core.search.gene.datetime.DateTimeGene +import org.evomaster.core.search.gene.datetime.TimeGene +import org.evomaster.core.search.gene.numeric.* +import org.evomaster.core.search.gene.string.StringGene + +/** + * Builds the gene used to generate the value of a Cassandra column, based on its CQL type. + * Only the scalar CQL types that can be inserted with a plain literal are handled: collections, + * user defined types, and the types that cannot be given an arbitrary value in an INSERT (eg a + * counter, which is only writable with an UPDATE) have no representation here. + */ +object CassandraColumnGeneBuilder { + + /** + * @return whether a gene can be built for [column], ie whether its CQL type is one of the + * scalar types handled here + */ + fun isSupported(column: CassandraColumn) = normalize(column.cqlType) in SUPPORTED_CQL_TYPES + + /** + * @throws IllegalArgumentException if the CQL type of [column] is not handled, as verifiable + * beforehand with [isSupported] + */ + fun buildGene(column: CassandraColumn): Gene { + + val name = column.name + + return when (normalize(column.cqlType)) { + "ascii", "text", "varchar" -> StringGene(name) + "tinyint" -> IntegerGene(name, min = Byte.MIN_VALUE.toInt(), max = Byte.MAX_VALUE.toInt()) + "smallint" -> IntegerGene(name, min = Short.MIN_VALUE.toInt(), max = Short.MAX_VALUE.toInt()) + "int" -> IntegerGene(name) + "bigint" -> LongGene(name) + "varint" -> BigIntegerGene(name) + "decimal" -> BigDecimalGene(name) + "float" -> FloatGene(name) + "double" -> DoubleGene(name) + "boolean" -> BooleanGene(name) + "uuid" -> UUIDGene(name) + /* + Only valid values are generated, as these genes are used to set up the state of the + database, and Cassandra would just reject an insertion carrying an invalid one. + */ + "timestamp" -> DateTimeGene(name, onlyValid = true) + "date" -> DateGene(name, onlyValidDates = true) + "time" -> TimeGene(name, onlyValidTimes = true) + else -> throw IllegalArgumentException("Cannot handle the CQL type of column $column") + } + } + + private fun normalize(cqlType: String) = cqlType.trim().lowercase() + + private val SUPPORTED_CQL_TYPES = setOf( + "ascii", "text", "varchar", + "tinyint", "smallint", "int", "bigint", "varint", "decimal", "float", "double", + "boolean", + "uuid", + "timestamp", "date", "time" + ) +} diff --git a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraDbAction.kt b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraDbAction.kt new file mode 100644 index 0000000000..a8dfbe4169 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraDbAction.kt @@ -0,0 +1,51 @@ +package org.evomaster.core.database.cassandra + +import org.evomaster.core.search.action.Action +import org.evomaster.core.search.action.EnvironmentAction +import org.evomaster.core.search.gene.Gene + +/** + * An action inserting a single row into a Cassandra table, used to set up the state of the database + * before the main actions of a test are executed. + */ +class CassandraDbAction( + /** + * The keyspace containing the table to insert the row into + */ + val keyspace: String, + /** + * The table to insert the row into + */ + val table: String, + /** + * The columns the row is composed of, ie the ones a value is generated for. + * There is exactly one gene per column, in the same order. + */ + val columns: List, + computedGenes: List? = null +) : EnvironmentAction(listOf()) { + + private val genes: List = (computedGenes ?: computeGenes()).also { addChildren(it) } + + init { + if (genes.size != columns.size) { + throw IllegalArgumentException("Mismatch between the ${columns.size} columns and the ${genes.size} genes") + } + } + + private fun computeGenes(): List { + return columns.map { CassandraColumnGeneBuilder.buildGene(it) } + } + + override fun getName(): String { + return "CASSANDRA_Insert_${keyspace}_${table}" + } + + override fun seeTopGenes(): List { + return genes + } + + override fun copyContent(): Action { + return CassandraDbAction(keyspace, table, columns, genes.map(Gene::copy)) + } +} diff --git a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraDbActionResult.kt b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraDbActionResult.kt new file mode 100644 index 0000000000..2279c2381e --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraDbActionResult.kt @@ -0,0 +1,36 @@ +package org.evomaster.core.database.cassandra + +import org.evomaster.core.search.action.Action +import org.evomaster.core.search.action.ActionResult + +/** + * Cassandra insert action execution result + */ +class CassandraDbActionResult : ActionResult { + + constructor(sourceLocalId: String, stopping: Boolean = false) : super(sourceLocalId, stopping) + constructor(other: CassandraDbActionResult) : super(other) + + companion object { + const val INSERT_CASSANDRA_EXECUTE_SUCCESSFULLY = "INSERT_CASSANDRA_EXECUTE_SUCCESSFULLY" + } + + override fun copy(): CassandraDbActionResult { + return CassandraDbActionResult(this) + } + + /** + * @param success specifies whether the INSERT CASSANDRA executed successfully + */ + fun setInsertExecutionResult(success: Boolean) = + addResultValue(INSERT_CASSANDRA_EXECUTE_SUCCESSFULLY, success.toString()) + + /** + * @return whether the Cassandra action executed successfully + */ + fun getInsertExecutionResult() = getResultValue(INSERT_CASSANDRA_EXECUTE_SUCCESSFULLY)?.toBoolean() ?: false + + override fun matchedType(action: Action): Boolean { + return action is CassandraDbAction + } +} diff --git a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraDbActionTransformer.kt b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraDbActionTransformer.kt new file mode 100644 index 0000000000..303955a75e --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraDbActionTransformer.kt @@ -0,0 +1,39 @@ +package org.evomaster.core.database.cassandra + +import org.evomaster.client.java.controller.api.dto.database.operations.CassandraDatabaseCommandDto +import org.evomaster.client.java.controller.api.dto.database.operations.CassandraInsertionDto +import org.evomaster.client.java.controller.api.dto.database.operations.CassandraInsertionEntryDto + +/** + * Transforms the Cassandra insert actions of an individual into the commands to be executed on the + * SUT side. + */ +object CassandraDbActionTransformer { + + fun transform(actions: List): CassandraDatabaseCommandDto { + + val insertionDtos = mutableListOf() + + for (action in actions) { + + val insertionDto = CassandraInsertionDto().apply { + keyspaceName = action.keyspace + tableName = action.table + } + + action.seeTopGenes() + .filter { it.isPrintable() } + .forEach { gene -> + val entry = CassandraInsertionEntryDto().apply { + columnName = gene.name + printableValue = CassandraLiteralRenderer.toCqlLiteral(gene) + } + insertionDto.data.add(entry) + } + + insertionDtos.add(insertionDto) + } + + return CassandraDatabaseCommandDto().apply { this.insertions = insertionDtos } + } +} diff --git a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraInsertBuilder.kt b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraInsertBuilder.kt new file mode 100644 index 0000000000..3aa5b36161 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraInsertBuilder.kt @@ -0,0 +1,54 @@ +package org.evomaster.core.database.cassandra + +import org.evomaster.core.logging.LoggingUtil +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +/** + * Builds the action inserting a row into a Cassandra table, based on the description of the + * columns of that table reported by the SUT driver. + */ +class CassandraInsertBuilder { + + companion object { + private val log: Logger = LoggerFactory.getLogger(CassandraInsertBuilder::class.java) + } + + /** + * The columns whose CQL type is not handled are left out of the insertion, as no value can be + * generated for them. The resulting insertion is still worth executing, since the remaining + * columns might be all that is needed, and a rejected insertion is already recorded as a failed + * one instead of stopping the search. + * + * Note that the genes of the returned action are not initialized yet, which is left to the + * caller, as it is done for the other types of database action. + */ + fun createCassandraInsertionAction(keyspace: String, table: String, tableSchema: String): CassandraDbAction { + + val columns = CassandraTableSchemaParser.parse(tableSchema) + + val (supported, unsupported) = columns.partition { CassandraColumnGeneBuilder.isSupported(it) } + + if (unsupported.isNotEmpty()) { + LoggingUtil.uniqueWarn( + log, + "Cannot generate data for some columns of $keyspace.$table, as their CQL type is not handled: {}", + unsupported.joinToString(", ") { "${it.name} ${it.cqlType}" } + ) + + /* + Cassandra requires a full primary key in an INSERT, so leaving out any of those + columns means the insertion is going to be rejected. + */ + if (unsupported.any { it.isPartitionKey || it.isClusteringColumn }) { + LoggingUtil.uniqueWarn( + log, + "Some of those columns are part of the primary key of {}, so the insertion will fail", + "$keyspace.$table" + ) + } + } + + return CassandraDbAction(keyspace, table, supported).apply { forceNewTaints() } + } +} diff --git a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraLiteralRenderer.kt b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraLiteralRenderer.kt new file mode 100644 index 0000000000..4bca18145a --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraLiteralRenderer.kt @@ -0,0 +1,45 @@ +package org.evomaster.core.database.cassandra + +import org.evomaster.core.search.gene.BooleanGene +import org.evomaster.core.search.gene.Gene +import org.evomaster.core.search.gene.UUIDGene +import org.evomaster.core.search.gene.datetime.DateGene +import org.evomaster.core.search.gene.datetime.DateTimeGene +import org.evomaster.core.search.gene.datetime.TimeGene +import org.evomaster.core.search.gene.numeric.NumberGene +import org.evomaster.core.search.gene.string.StringGene + +/** + * Renders the value of a gene as a CQL literal, ie as it would be written inside a CQL statement. + * + * This is needed because such literals are inserted verbatim into the INSERT command built on the + * client side, and how a value has to be written depends on its type: text and the temporal types + * are enclosed in single quotes, whereas numbers, booleans and uuids are not. + */ +object CassandraLiteralRenderer { + + private const val SINGLE_QUOTE = "'" + + /** + * In CQL, a single quote inside a text literal is escaped by doubling it. + */ + private const val ESCAPED_SINGLE_QUOTE = "''" + + /** + * @throws IllegalArgumentException if there is no known CQL representation for [gene], which + * should not happen for the genes built by [CassandraColumnGeneBuilder] + */ + fun toCqlLiteral(gene: Gene): String { + + val value = gene.getValueAsRawString() + + return when (gene) { + is StringGene, is DateGene, is TimeGene, is DateTimeGene -> quote(value) + is BooleanGene, is UUIDGene, is NumberGene<*> -> value + else -> throw IllegalArgumentException("Cannot render a CQL literal for a gene of type ${gene.javaClass.simpleName}") + } + } + + private fun quote(value: String) = + SINGLE_QUOTE + value.replace(SINGLE_QUOTE, ESCAPED_SINGLE_QUOTE) + SINGLE_QUOTE +} diff --git a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraTableSchemaParser.kt b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraTableSchemaParser.kt new file mode 100644 index 0000000000..2557c68d2c --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraTableSchemaParser.kt @@ -0,0 +1,97 @@ +package org.evomaster.core.database.cassandra + +/** + * Recovers the columns of a Cassandra table from the flat schema description string reported by the + * SUT driver, ie the inverse of how [CassandraColumn]s are rendered on the client side, where each + * column becomes "name type" optionally followed by a " PARTITION KEY" and/or " CLUSTERING" marker, + * and columns are joined with ", ". + */ +object CassandraTableSchemaParser { + + private const val COLUMN_SEPARATOR = ',' + + private const val COLUMN_NAME_TYPE_SEPARATOR = ' ' + + private const val PARTITION_KEY_COLUMN_SUFFIX = " PARTITION KEY" + + private const val CLUSTERING_COLUMN_SUFFIX = " CLUSTERING" + + private const val TYPE_PARAMETERS_START = '<' + + private const val TYPE_PARAMETERS_END = '>' + + /** + * @param tableSchema the description of all the columns of a table, as reported by the SUT driver + * @return the columns described in [tableSchema], in the same order + * @throws IllegalArgumentException if any of the described columns is malformed + */ + fun parse(tableSchema: String): List { + + return splitColumns(tableSchema) + .map { it.trim() } + .filter { it.isNotEmpty() } + .map { parseColumn(it) } + } + + /** + * Splits on the separator between columns, ignoring the separators nested inside a type + * parameter list, as a collection type is itself rendered with them, eg "map". + */ + private fun splitColumns(tableSchema: String): List { + + val columns = mutableListOf() + val current = StringBuilder() + var depth = 0 + + for (c in tableSchema) { + when { + c == TYPE_PARAMETERS_START -> { + depth++ + current.append(c) + } + c == TYPE_PARAMETERS_END -> { + depth-- + current.append(c) + } + c == COLUMN_SEPARATOR && depth == 0 -> { + columns.add(current.toString()) + current.clear() + } + else -> current.append(c) + } + } + columns.add(current.toString()) + + return columns + } + + private fun parseColumn(description: String): CassandraColumn { + + var remainder = description + + /* + The two markers are appended in this order, so they have to be peeled off in reverse. + Both can in principle be present, as they are rendered independently of each other. + */ + val isClusteringColumn = remainder.endsWith(CLUSTERING_COLUMN_SUFFIX) + if (isClusteringColumn) { + remainder = remainder.removeSuffix(CLUSTERING_COLUMN_SUFFIX) + } + val isPartitionKey = remainder.endsWith(PARTITION_KEY_COLUMN_SUFFIX) + if (isPartitionKey) { + remainder = remainder.removeSuffix(PARTITION_KEY_COLUMN_SUFFIX) + } + + val separatorIndex = remainder.indexOf(COLUMN_NAME_TYPE_SEPARATOR) + if (separatorIndex <= 0 || separatorIndex == remainder.length - 1) { + throw IllegalArgumentException("Malformed description of a Cassandra column: $description") + } + + return CassandraColumn( + name = remainder.substring(0, separatorIndex), + cqlType = remainder.substring(separatorIndex + 1), + isPartitionKey = isPartitionKey, + isClusteringColumn = isClusteringColumn + ) + } +} From b55b6ddfea06b13ca04e74faa33bef1703f581d0 Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Sun, 23 Aug 2026 23:37:31 -0300 Subject: [PATCH 2/8] Add tests for Cassandra Actions --- .../CassandraColumnGeneBuilderTest.kt | 103 ++++++++++++++++++ .../CassandraDbActionTransformerTest.kt | 71 ++++++++++++ .../cassandra/CassandraInsertBuilderTest.kt | 77 +++++++++++++ .../cassandra/CassandraLiteralRendererTest.kt | 83 ++++++++++++++ .../CassandraTableSchemaParserTest.kt | 85 +++++++++++++++ 5 files changed, 419 insertions(+) create mode 100644 core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraColumnGeneBuilderTest.kt create mode 100644 core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraDbActionTransformerTest.kt create mode 100644 core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraInsertBuilderTest.kt create mode 100644 core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraLiteralRendererTest.kt create mode 100644 core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraTableSchemaParserTest.kt diff --git a/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraColumnGeneBuilderTest.kt b/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraColumnGeneBuilderTest.kt new file mode 100644 index 0000000000..f905c74df7 --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraColumnGeneBuilderTest.kt @@ -0,0 +1,103 @@ +package org.evomaster.core.database.cassandra + +import org.evomaster.core.search.gene.BooleanGene +import org.evomaster.core.search.gene.Gene +import org.evomaster.core.search.gene.UUIDGene +import org.evomaster.core.search.gene.datetime.DateGene +import org.evomaster.core.search.gene.datetime.DateTimeGene +import org.evomaster.core.search.gene.datetime.TimeGene +import org.evomaster.core.search.gene.numeric.BigDecimalGene +import org.evomaster.core.search.gene.numeric.BigIntegerGene +import org.evomaster.core.search.gene.numeric.DoubleGene +import org.evomaster.core.search.gene.numeric.FloatGene +import org.evomaster.core.search.gene.numeric.IntegerGene +import org.evomaster.core.search.gene.numeric.LongGene +import org.evomaster.core.search.gene.string.StringGene +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class CassandraColumnGeneBuilderTest { + + private fun buildFor(cqlType: String): Gene = + CassandraColumnGeneBuilder.buildGene(CassandraColumn("aColumn", cqlType)) + + @Test + fun testTextTypes() { + listOf("ascii", "text", "varchar").forEach { + assertTrue(buildFor(it) is StringGene, "unexpected gene for $it") + } + } + + @Test + fun testIntegerTypes() { + assertTrue(buildFor("tinyint") is IntegerGene) + assertTrue(buildFor("smallint") is IntegerGene) + assertTrue(buildFor("int") is IntegerGene) + } + + @Test + fun testBoundsOfNarrowIntegerTypes() { + val tinyint = buildFor("tinyint") as IntegerGene + assertEquals(Byte.MIN_VALUE.toInt(), tinyint.min) + assertEquals(Byte.MAX_VALUE.toInt(), tinyint.max) + + val smallint = buildFor("smallint") as IntegerGene + assertEquals(Short.MIN_VALUE.toInt(), smallint.min) + assertEquals(Short.MAX_VALUE.toInt(), smallint.max) + } + + @Test + fun testOtherNumericTypes() { + assertTrue(buildFor("bigint") is LongGene) + assertTrue(buildFor("varint") is BigIntegerGene) + assertTrue(buildFor("decimal") is BigDecimalGene) + assertTrue(buildFor("float") is FloatGene) + assertTrue(buildFor("double") is DoubleGene) + } + + @Test + fun testBooleanAndUuidTypes() { + assertTrue(buildFor("boolean") is BooleanGene) + assertTrue(buildFor("uuid") is UUIDGene) + } + + @Test + fun testTemporalTypes() { + assertTrue(buildFor("timestamp") is DateTimeGene) + assertTrue(buildFor("date") is DateGene) + assertTrue(buildFor("time") is TimeGene) + } + + @Test + fun testTypeNameIsNormalized() { + assertTrue(buildFor(" TEXT ") is StringGene) + } + + @Test + fun testGeneKeepsTheNameOfTheColumn() { + val gene = CassandraColumnGeneBuilder.buildGene(CassandraColumn("firstName", "text")) + assertEquals("firstName", gene.name) + } + + /** + * A counter is only writable with an UPDATE, and a timeuuid needs a value that a plain uuid + * gene would not produce, so neither can be given an arbitrary value in an insertion. + */ + @Test + fun testUnsupportedTypes() { + listOf("counter", "timeuuid", "blob", "inet", "duration", "list", "frozen").forEach { + assertFalse(CassandraColumnGeneBuilder.isSupported(CassandraColumn("aColumn", it)), "$it should not be supported") + assertThrows("no exception for $it") { buildFor(it) } + } + } + + @Test + fun testSupportedTypesAreReportedAsSuch() { + listOf("text", "int", "uuid", "timestamp", "boolean").forEach { + assertTrue(CassandraColumnGeneBuilder.isSupported(CassandraColumn("aColumn", it)), "$it should be supported") + } + } +} diff --git a/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraDbActionTransformerTest.kt b/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraDbActionTransformerTest.kt new file mode 100644 index 0000000000..67efab6fe8 --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraDbActionTransformerTest.kt @@ -0,0 +1,71 @@ +package org.evomaster.core.database.cassandra + +import org.evomaster.core.search.gene.numeric.IntegerGene +import org.evomaster.core.search.gene.string.StringGene +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class CassandraDbActionTransformerTest { + + private fun anAction(): CassandraDbAction { + return CassandraDbAction( + "ks", "users", + listOf(CassandraColumn("name", "text"), CassandraColumn("age", "int")), + listOf(StringGene("name", "Alice"), IntegerGene("age", 42)) + ) + } + + @Test + fun testNoAction() { + assertTrue(CassandraDbActionTransformer.transform(listOf()).insertions.isEmpty()) + } + + @Test + fun testKeyspaceAndTableAreReported() { + val dto = CassandraDbActionTransformer.transform(listOf(anAction())) + + assertEquals(1, dto.insertions.size) + assertEquals("ks", dto.insertions[0].keyspaceName) + assertEquals("users", dto.insertions[0].tableName) + } + + @Test + fun testOneEntryPerColumnWithItsCqlLiteral() { + val dto = CassandraDbActionTransformer.transform(listOf(anAction())) + + val data = dto.insertions[0].data + assertEquals(2, data.size) + + assertEquals("name", data[0].columnName) + assertEquals("'Alice'", data[0].printableValue) + + assertEquals("age", data[1].columnName) + assertEquals("42", data[1].printableValue) + } + + @Test + fun testSeveralActionsKeepTheirOrder() { + val other = CassandraDbAction( + "ks", "events", + listOf(CassandraColumn("note", "text")), + listOf(StringGene("note", "hello")) + ) + + val dto = CassandraDbActionTransformer.transform(listOf(anAction(), other)) + + assertEquals(2, dto.insertions.size) + assertEquals("users", dto.insertions[0].tableName) + assertEquals("events", dto.insertions[1].tableName) + } + + @Test + fun testActionWithNoColumn() { + val action = CassandraDbAction("ks", "empty", listOf(), listOf()) + + val dto = CassandraDbActionTransformer.transform(listOf(action)) + + assertEquals(1, dto.insertions.size) + assertTrue(dto.insertions[0].data.isEmpty()) + } +} diff --git a/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraInsertBuilderTest.kt b/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraInsertBuilderTest.kt new file mode 100644 index 0000000000..a2d65adf0c --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraInsertBuilderTest.kt @@ -0,0 +1,77 @@ +package org.evomaster.core.database.cassandra + +import org.evomaster.core.search.gene.UUIDGene +import org.evomaster.core.search.gene.string.StringGene +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class CassandraInsertBuilderTest { + + private val builder = CassandraInsertBuilder() + + @Test + fun testOneGenePerColumn() { + val action = builder.createCassandraInsertionAction("ks", "users", "id uuid PARTITION KEY, name text") + + assertEquals(listOf("id", "name"), action.seeTopGenes().map { it.name }) + assertTrue(action.seeTopGenes()[0] is UUIDGene) + assertTrue(action.seeTopGenes()[1] is StringGene) + } + + @Test + fun testKeyspaceAndTableAreKept() { + val action = builder.createCassandraInsertionAction("ks", "users", "id uuid PARTITION KEY") + + assertEquals("ks", action.keyspace) + assertEquals("users", action.table) + } + + @Test + fun testActionName() { + val action = builder.createCassandraInsertionAction("ks", "users", "id uuid PARTITION KEY") + + assertEquals("CASSANDRA_Insert_ks_users", action.getName()) + } + + @Test + fun testKeyRolesAreKept() { + val action = builder.createCassandraInsertionAction( + "ks", "events", "id uuid PARTITION KEY, created timestamp CLUSTERING, note text") + + assertTrue(action.columns[0].isPartitionKey) + assertTrue(action.columns[1].isClusteringColumn) + assertTrue(!action.columns[2].isPartitionKey && !action.columns[2].isClusteringColumn) + } + + /** + * No value can be generated for a column whose type is not handled, so it is just left out of + * the insertion instead of preventing the other columns from being inserted. + */ + @Test + fun testColumnsWithUnsupportedTypeAreSkipped() { + val action = builder.createCassandraInsertionAction( + "ks", "users", "id uuid PARTITION KEY, picture blob, name text") + + assertEquals(listOf("id", "name"), action.seeTopGenes().map { it.name }) + assertEquals(listOf("id", "name"), action.columns.map { it.name }) + } + + @Test + fun testTableWithNoSupportedColumn() { + val action = builder.createCassandraInsertionAction("ks", "blobs", "content blob") + + assertTrue(action.seeTopGenes().isEmpty()) + } + + @Test + fun testCopyKeepsTheColumns() { + val action = builder.createCassandraInsertionAction("ks", "users", "id uuid PARTITION KEY, name text") + val copy = action.copy() as CassandraDbAction + + assertEquals(action.keyspace, copy.keyspace) + assertEquals(action.table, copy.table) + assertEquals(action.columns, copy.columns) + assertEquals(action.seeTopGenes().map { it.name }, copy.seeTopGenes().map { it.name }) + } +} diff --git a/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraLiteralRendererTest.kt b/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraLiteralRendererTest.kt new file mode 100644 index 0000000000..5782cb46bc --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraLiteralRendererTest.kt @@ -0,0 +1,83 @@ +package org.evomaster.core.database.cassandra + +import org.evomaster.core.search.gene.BooleanGene +import org.evomaster.core.search.gene.ObjectGene +import org.evomaster.core.search.gene.UUIDGene +import org.evomaster.core.search.gene.datetime.DateGene +import org.evomaster.core.search.gene.datetime.DateTimeGene +import org.evomaster.core.search.gene.datetime.TimeGene +import org.evomaster.core.search.gene.numeric.DoubleGene +import org.evomaster.core.search.gene.numeric.IntegerGene +import org.evomaster.core.search.gene.numeric.LongGene +import org.evomaster.core.search.gene.string.StringGene +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class CassandraLiteralRendererTest { + + @Test + fun testTextIsQuoted() { + assertEquals("'Alice'", CassandraLiteralRenderer.toCqlLiteral(StringGene("name", "Alice"))) + } + + @Test + fun testEmptyTextIsQuoted() { + assertEquals("''", CassandraLiteralRenderer.toCqlLiteral(StringGene("name", ""))) + } + + /** + * In CQL, a single quote inside a text literal is escaped by doubling it. + */ + @Test + fun testSingleQuoteInsideTextIsDoubled() { + assertEquals("'l''Alice'", CassandraLiteralRenderer.toCqlLiteral(StringGene("name", "l'Alice"))) + } + + @Test + fun testSeveralSingleQuotesInsideTextAreDoubled() { + assertEquals("'''a'''", CassandraLiteralRenderer.toCqlLiteral(StringGene("name", "'a'"))) + } + + @Test + fun testNumbersAreNotQuoted() { + assertEquals("42", CassandraLiteralRenderer.toCqlLiteral(IntegerGene("age", 42))) + assertEquals("-7", CassandraLiteralRenderer.toCqlLiteral(IntegerGene("delta", -7))) + assertEquals("123", CassandraLiteralRenderer.toCqlLiteral(LongGene("amount", 123L))) + } + + @Test + fun testDoubleIsNotQuoted() { + val gene = DoubleGene("ratio", 1.5) + assertEquals(gene.getValueAsRawString(), CassandraLiteralRenderer.toCqlLiteral(gene)) + } + + @Test + fun testBooleanIsNotQuoted() { + assertEquals("true", CassandraLiteralRenderer.toCqlLiteral(BooleanGene("flag", true))) + assertEquals("false", CassandraLiteralRenderer.toCqlLiteral(BooleanGene("flag", false))) + } + + /** + * A uuid literal is written without quotes in CQL. + */ + @Test + fun testUuidIsNotQuoted() { + val gene = UUIDGene("id") + assertEquals(gene.getValueAsRawString(), CassandraLiteralRenderer.toCqlLiteral(gene)) + } + + @Test + fun testTemporalValuesAreQuoted() { + listOf(DateTimeGene("created"), DateGene("day"), TimeGene("moment")).forEach { + assertEquals("'${it.getValueAsRawString()}'", CassandraLiteralRenderer.toCqlLiteral(it)) + } + } + + @Test + fun testGeneWithNoCqlRepresentationIsRejected() { + assertThrows { + CassandraLiteralRenderer.toCqlLiteral(ObjectGene("obj", listOf())) + } + } +} diff --git a/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraTableSchemaParserTest.kt b/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraTableSchemaParserTest.kt new file mode 100644 index 0000000000..f98ba68f67 --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraTableSchemaParserTest.kt @@ -0,0 +1,85 @@ +package org.evomaster.core.database.cassandra + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class CassandraTableSchemaParserTest { + + @Test + fun testEmptySchema() { + assertTrue(CassandraTableSchemaParser.parse("").isEmpty()) + } + + @Test + fun testSingleRegularColumn() { + val columns = CassandraTableSchemaParser.parse("name text") + + assertEquals(1, columns.size) + assertEquals(CassandraColumn("name", "text"), columns[0]) + } + + @Test + fun testPartitionKeyColumn() { + val columns = CassandraTableSchemaParser.parse("id uuid PARTITION KEY") + + assertEquals(listOf(CassandraColumn("id", "uuid", isPartitionKey = true)), columns) + } + + @Test + fun testClusteringColumn() { + val columns = CassandraTableSchemaParser.parse("created timestamp CLUSTERING") + + assertEquals(listOf(CassandraColumn("created", "timestamp", isClusteringColumn = true)), columns) + } + + @Test + fun testColumnMarkedBothAsPartitionKeyAndClustering() { + val columns = CassandraTableSchemaParser.parse("id uuid PARTITION KEY CLUSTERING") + + assertEquals( + listOf(CassandraColumn("id", "uuid", isPartitionKey = true, isClusteringColumn = true)), + columns + ) + } + + @Test + fun testSeveralColumnsKeepTheirOrder() { + val columns = CassandraTableSchemaParser.parse("id uuid PARTITION KEY, name text, created timestamp CLUSTERING") + + assertEquals( + listOf( + CassandraColumn("id", "uuid", isPartitionKey = true), + CassandraColumn("name", "text"), + CassandraColumn("created", "timestamp", isClusteringColumn = true) + ), + columns + ) + } + + /** + * The type of a collection is itself rendered with the same separator used between columns. + */ + @Test + fun testCollectionTypeIsNotSplit() { + val columns = CassandraTableSchemaParser.parse("id uuid PARTITION KEY, data map") + + assertEquals(2, columns.size) + assertEquals(CassandraColumn("data", "map"), columns[1]) + } + + @Test + fun testNestedCollectionTypeIsNotSplit() { + val columns = CassandraTableSchemaParser.parse("data map>>, name text") + + assertEquals(2, columns.size) + assertEquals(CassandraColumn("data", "map>>"), columns[0]) + assertEquals(CassandraColumn("name", "text"), columns[1]) + } + + @Test + fun testColumnWithNoTypeIsRejected() { + assertThrows { CassandraTableSchemaParser.parse("name") } + } +} From 25e99b4f1d44d60a39d6e113fbdbf04e7c0352a3 Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Mon, 24 Aug 2026 00:04:30 -0300 Subject: [PATCH 3/8] Add CassandraWriter --- .../evomaster/core/output/CassandraWriter.kt | 97 ++++++++++++ .../core/output/CassandraWriterTest.kt | 141 ++++++++++++++++++ 2 files changed, 238 insertions(+) create mode 100644 core/src/main/kotlin/org/evomaster/core/output/CassandraWriter.kt create mode 100644 core/src/test/kotlin/org/evomaster/core/output/CassandraWriterTest.kt diff --git a/core/src/main/kotlin/org/evomaster/core/output/CassandraWriter.kt b/core/src/main/kotlin/org/evomaster/core/output/CassandraWriter.kt new file mode 100644 index 0000000000..5b796e43a3 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/output/CassandraWriter.kt @@ -0,0 +1,97 @@ +package org.evomaster.core.output + +import org.apache.commons.text.StringEscapeUtils +import org.evomaster.core.database.cassandra.CassandraLiteralRenderer +import org.evomaster.core.search.action.EvaluatedCassandraDbAction + +/** + * Class used to generate the code in the test dealing with insertion of + * data into CASSANDRA databases. + * + * Note that the generated code calls a method to execute the insertions on the SUT controller, which + * does not exist yet, as the wiring of Cassandra into the controller is handled separately. Until + * that is in place, the tests generated for an individual with Cassandra actions do not compile. + */ +object CassandraWriter { + + /** + * generate cassandra insert actions into test case based on [cassandraDbInitialization] + * @param format is the format of tests to be generated + * @param cassandraDbInitialization contains the db actions to be generated + * @param lines is used to save generated textual lines with respects to [cassandraDbInitialization] + * @param groupIndex specifies an index of a group of this [cassandraDbInitialization] + * @param insertionVars is a list of previous variable names of the db actions (Pair.first) and corresponding results (Pair.second) + * @param skipFailure specifies whether to skip failure tests + */ + fun handleCassandraDbInitialization( + format: OutputFormat, + cassandraDbInitialization: List, + lines: Lines, + groupIndex: String = "", + insertionVars: MutableList>, + skipFailure: Boolean + ) { + + if (cassandraDbInitialization.isEmpty() + || cassandraDbInitialization.none { !skipFailure || it.cassandraResult.getInsertExecutionResult() }) { + return + } + + val insertionVar = "insertions_cassandra${groupIndex}" + val insertionVarResult = "${insertionVar}_result" + val previousVar = insertionVars.joinToString(", ") { it.first } + + cassandraDbInitialization + .filter { !skipFailure || it.cassandraResult.getInsertExecutionResult() } + .forEachIndexed { index, evaluatedCassandraDbAction -> + + lines.add( + when { + index == 0 && format.isJava() -> "List $insertionVar = cassandra($previousVar)" + index == 0 && format.isKotlin() -> "val $insertionVar = cassandra($previousVar)" + else -> ".and()" + } + ".insertInto(\"${evaluatedCassandraDbAction.cassandraAction.keyspace}\"" + ", " + + "\"${evaluatedCassandraDbAction.cassandraAction.table}\")" + ) + + if (index == 0) { + lines.indent() + } + + lines.indented { + evaluatedCassandraDbAction.action.seeTopGenes() + .filter { it.isPrintable() } + .forEach { g -> + val printableValue = escape(CassandraLiteralRenderer.toCqlLiteral(g), format) + lines.add(".d(\"${g.name}\", \"$printableValue\")") + } + } + } + + lines.add(".dtos()") + lines.appendSemicolon() + + lines.deindent() + + lines.add( + when { + format.isJava() -> "CassandraInsertionResultsDto " + format.isKotlin() -> "val " + else -> throw IllegalStateException("Not support cassandra insertions generation for $format") + } + "$insertionVarResult = controller.execInsertionsIntoCassandraDatabase($insertionVar)" + ) + lines.appendSemicolon() + + insertionVars.add(insertionVar to insertionVarResult) + } + + /** + * The CQL literal is embedded in a string literal of the generated test, so it has to be + * escaped for the language such a test is written in. + */ + private fun escape(value: String, format: OutputFormat): String { + return StringEscapeUtils.escapeJava(value).let { + if (format.isKotlin()) it.replace("$", "\\$") else it + } + } +} diff --git a/core/src/test/kotlin/org/evomaster/core/output/CassandraWriterTest.kt b/core/src/test/kotlin/org/evomaster/core/output/CassandraWriterTest.kt new file mode 100644 index 0000000000..b8b4f40487 --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/output/CassandraWriterTest.kt @@ -0,0 +1,141 @@ +package org.evomaster.core.output + +import org.evomaster.core.database.cassandra.CassandraColumn +import org.evomaster.core.database.cassandra.CassandraDbAction +import org.evomaster.core.database.cassandra.CassandraDbActionResult +import org.evomaster.core.search.action.EvaluatedCassandraDbAction +import org.evomaster.core.search.gene.Gene +import org.evomaster.core.search.gene.numeric.IntegerGene +import org.evomaster.core.search.gene.string.StringGene +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class CassandraWriterTest { + + private var counter = 0 + + private fun makeEvaluated( + keyspace: String = "ks", + table: String = "users", + columns: List = listOf(CassandraColumn("name", "text")), + genes: List = listOf(StringGene("name", "Alice")), + success: Boolean = true + ): EvaluatedCassandraDbAction { + val action = CassandraDbAction(keyspace, table, columns, genes) + action.setLocalId("test-cassandra-action-${counter++}") + val result = CassandraDbActionResult(action.getLocalId()).also { it.setInsertExecutionResult(success) } + return EvaluatedCassandraDbAction(action, result) + } + + private fun write( + actions: List, + format: OutputFormat = OutputFormat.KOTLIN_JUNIT_5, + insertionVars: MutableList> = mutableListOf(), + skipFailure: Boolean = false, + groupIndex: String = "" + ): String { + val lines = Lines(format) + CassandraWriter.handleCassandraDbInitialization(format, actions, lines, groupIndex, insertionVars, skipFailure) + return lines.toString() + } + + @Test + fun testEmptyListGeneratesNothing() { + assertTrue(write(emptyList()).isBlank()) + } + + @Test + fun testAllFailedWithSkipFailureGeneratesNothing() { + assertTrue(write(listOf(makeEvaluated(success = false)), skipFailure = true).isBlank()) + } + + @Test + fun testFailedInsertionIsKeptWhenNotSkipping() { + assertTrue(write(listOf(makeEvaluated(success = false))).contains(".insertInto(\"ks\", \"users\")")) + } + + @Test + fun testKotlinOutput() { + val output = write(listOf(makeEvaluated())) + + assertTrue(output.contains("val insertions_cassandra = cassandra()")) + assertTrue(output.contains(".insertInto(\"ks\", \"users\")")) + assertTrue(output.contains(".d(\"name\", \"'Alice'\")")) + assertTrue(output.contains(".dtos()")) + assertTrue(output.contains("val insertions_cassandra_result = controller.execInsertionsIntoCassandraDatabase(insertions_cassandra)")) + } + + @Test + fun testJavaOutput() { + val output = write(listOf(makeEvaluated()), format = OutputFormat.JAVA_JUNIT_5) + + assertTrue(output.contains("List insertions_cassandra = cassandra()")) + assertTrue(output.contains("CassandraInsertionResultsDto insertions_cassandra_result = controller.execInsertionsIntoCassandraDatabase(insertions_cassandra)")) + } + + @Test + fun testOneColumnPerGene() { + val output = write( + listOf( + makeEvaluated( + columns = listOf(CassandraColumn("name", "text"), CassandraColumn("age", "int")), + genes = listOf(StringGene("name", "Alice"), IntegerGene("age", 42)) + ) + ) + ) + + assertTrue(output.contains(".d(\"name\", \"'Alice'\")")) + assertTrue(output.contains(".d(\"age\", \"42\")")) + } + + @Test + fun testSeveralActionsAreChained() { + val output = write(listOf(makeEvaluated(), makeEvaluated(table = "events"))) + + assertTrue(output.contains(".insertInto(\"ks\", \"users\")")) + assertTrue(output.contains(".and().insertInto(\"ks\", \"events\")")) + } + + /** + * The CQL literal ends up inside a string literal of the generated test, so it has to be + * escaped for the language that test is written in. + */ + @Test + fun testValueIsEscapedForTheGeneratedTest() { + val output = write( + listOf(makeEvaluated(genes = listOf(StringGene("name", "a\"b")))) + ) + + assertTrue(output.contains(".d(\"name\", \"'a\\\"b'\")")) + } + + @Test + fun testDollarIsEscapedInKotlinOnly() { + val genes = listOf(StringGene("name", "a\$b")) + + val kotlin = write(listOf(makeEvaluated(genes = genes)), format = OutputFormat.KOTLIN_JUNIT_5) + assertTrue(kotlin.contains("\\$")) + + val java = write(listOf(makeEvaluated(genes = genes)), format = OutputFormat.JAVA_JUNIT_5) + assertFalse(java.contains("\\$")) + } + + @Test + fun testInsertionVarIsRegisteredForFollowingGroups() { + val insertionVars = mutableListOf>() + + write(listOf(makeEvaluated()), insertionVars = insertionVars) + + assertTrue(insertionVars.contains("insertions_cassandra" to "insertions_cassandra_result")) + } + + @Test + fun testPreviousInsertionVarsArePassedOn() { + val insertionVars = mutableListOf("insertions" to "insertionsresult") + + val output = write(listOf(makeEvaluated()), insertionVars = insertionVars, groupIndex = "1") + + assertTrue(output.contains("val insertions_cassandra1 = cassandra(insertions)")) + } +} From 0f98ceef1b6ba6db0184e2ef0a0fc64bf81c6a0b Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Mon, 24 Aug 2026 00:35:28 -0300 Subject: [PATCH 4/8] Add Cassandra evaluated action --- .../evomaster/core/search/action/EvaluatedAction.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/action/EvaluatedAction.kt b/core/src/main/kotlin/org/evomaster/core/search/action/EvaluatedAction.kt index 7bda63b2ba..b5c16be96b 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/action/EvaluatedAction.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/action/EvaluatedAction.kt @@ -1,11 +1,13 @@ package org.evomaster.core.search.action -import org.evomaster.core.database.sql.SqlAction -import org.evomaster.core.database.sql.SqlActionResult +import org.evomaster.core.database.cassandra.CassandraDbAction +import org.evomaster.core.database.cassandra.CassandraDbActionResult import org.evomaster.core.database.mongo.MongoDbAction import org.evomaster.core.database.mongo.MongoDbActionResult import org.evomaster.core.database.redis.RedisDbAction import org.evomaster.core.database.redis.RedisDbActionResult +import org.evomaster.core.database.sql.SqlAction +import org.evomaster.core.database.sql.SqlActionResult open class EvaluatedAction(val action: Action, val result: ActionResult){ @@ -25,4 +27,6 @@ class EvaluatedDbAction(val sqlAction: SqlAction, val sqlResult: SqlActionResult class EvaluatedMongoDbAction(val mongoAction: MongoDbAction, val mongoResult: MongoDbActionResult) : EvaluatedAction(mongoAction, mongoResult) -class EvaluatedRedisDbAction(val redisAction: RedisDbAction, val redisResult: RedisDbActionResult) : EvaluatedAction(redisAction, redisResult) \ No newline at end of file +class EvaluatedRedisDbAction(val redisAction: RedisDbAction, val redisResult: RedisDbActionResult) : EvaluatedAction(redisAction, redisResult) + +class EvaluatedCassandraDbAction(val cassandraAction: CassandraDbAction, val cassandraResult: CassandraDbActionResult) : EvaluatedAction(cassandraAction, cassandraResult) \ No newline at end of file From 6c189c18abbaefa20dc1ec4dab54d4c3ac13e904 Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Fri, 28 Aug 2026 19:53:49 -0300 Subject: [PATCH 5/8] Improve building of insertions --- .../cassandra/CassandraColumnGeneBuilder.kt | 74 ++++++++++--------- .../database/cassandra/CassandraDbAction.kt | 5 ++ .../cassandra/CassandraInsertBuilder.kt | 61 +++++++++++---- .../cassandra/CassandraLiteralRenderer.kt | 5 +- .../cassandra/CassandraTableSchemaParser.kt | 13 ++++ .../CassandraColumnGeneBuilderTest.kt | 11 ++- .../cassandra/CassandraInsertBuilderTest.kt | 40 +++++++++- .../cassandra/CassandraLiteralRendererTest.kt | 19 +++++ .../CassandraTableSchemaParserTest.kt | 14 ++++ 9 files changed, 186 insertions(+), 56 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraColumnGeneBuilder.kt b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraColumnGeneBuilder.kt index b07890fb50..3f9c897b25 100644 --- a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraColumnGeneBuilder.kt +++ b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraColumnGeneBuilder.kt @@ -3,6 +3,7 @@ package org.evomaster.core.database.cassandra import org.evomaster.core.search.gene.BooleanGene import org.evomaster.core.search.gene.Gene import org.evomaster.core.search.gene.UUIDGene +import org.evomaster.core.search.gene.cassandra.CqlDurationGene import org.evomaster.core.search.gene.datetime.DateGene import org.evomaster.core.search.gene.datetime.DateTimeGene import org.evomaster.core.search.gene.datetime.TimeGene @@ -11,17 +12,50 @@ import org.evomaster.core.search.gene.string.StringGene /** * Builds the gene used to generate the value of a Cassandra column, based on its CQL type. - * Only the scalar CQL types that can be inserted with a plain literal are handled: collections, - * user defined types, and the types that cannot be given an arbitrary value in an INSERT (eg a - * counter, which is only writable with an UPDATE) have no representation here. + * + * Two different reasons keep a CQL type out of the ones handled here: + * - the value of a column of that type cannot be generated at all, ie a counter, which is only + * writable with an UPDATE, and a timeuuid, which requires a version 1 UUID, whereas [UUIDGene] + * generates a random one; + * - no gene generating a value of that type has been written yet, ie blob, inet, the collections + * and the user defined types. */ object CassandraColumnGeneBuilder { + /** + * How the gene generating the value of a column is built, for each of the CQL types handled + * here, keyed by the normalized name of the type. Being the single place where such types are + * enumerated, it is also what [isSupported] answers from, so that the two cannot disagree. + */ + private val GENE_BUILDERS: Map Gene> = mapOf( + "ascii" to { name -> StringGene(name) }, + "text" to { name -> StringGene(name) }, + "varchar" to { name -> StringGene(name) }, + "tinyint" to { name -> IntegerGene(name, min = Byte.MIN_VALUE.toInt(), max = Byte.MAX_VALUE.toInt()) }, + "smallint" to { name -> IntegerGene(name, min = Short.MIN_VALUE.toInt(), max = Short.MAX_VALUE.toInt()) }, + "int" to { name -> IntegerGene(name) }, + "bigint" to { name -> LongGene(name) }, + "varint" to { name -> BigIntegerGene(name) }, + "decimal" to { name -> BigDecimalGene(name) }, + "float" to { name -> FloatGene(name) }, + "double" to { name -> DoubleGene(name) }, + "boolean" to { name -> BooleanGene(name) }, + "uuid" to { name -> UUIDGene(name) }, + /* + Only valid values are generated, as these genes are used to set up the state of the + database, and Cassandra would just reject an insertion carrying an invalid one. + */ + "timestamp" to { name -> DateTimeGene(name, onlyValid = true) }, + "date" to { name -> DateGene(name, onlyValidDates = true) }, + "time" to { name -> TimeGene(name, onlyValidTimes = true) }, + "duration" to { name -> CqlDurationGene(name) } + ) + /** * @return whether a gene can be built for [column], ie whether its CQL type is one of the * scalar types handled here */ - fun isSupported(column: CassandraColumn) = normalize(column.cqlType) in SUPPORTED_CQL_TYPES + fun isSupported(column: CassandraColumn) = normalize(column.cqlType) in GENE_BUILDERS /** * @throws IllegalArgumentException if the CQL type of [column] is not handled, as verifiable @@ -29,38 +63,12 @@ object CassandraColumnGeneBuilder { */ fun buildGene(column: CassandraColumn): Gene { - val name = column.name + val builder = GENE_BUILDERS[normalize(column.cqlType)] + ?: throw IllegalArgumentException("Cannot handle the CQL type of column $column") - return when (normalize(column.cqlType)) { - "ascii", "text", "varchar" -> StringGene(name) - "tinyint" -> IntegerGene(name, min = Byte.MIN_VALUE.toInt(), max = Byte.MAX_VALUE.toInt()) - "smallint" -> IntegerGene(name, min = Short.MIN_VALUE.toInt(), max = Short.MAX_VALUE.toInt()) - "int" -> IntegerGene(name) - "bigint" -> LongGene(name) - "varint" -> BigIntegerGene(name) - "decimal" -> BigDecimalGene(name) - "float" -> FloatGene(name) - "double" -> DoubleGene(name) - "boolean" -> BooleanGene(name) - "uuid" -> UUIDGene(name) - /* - Only valid values are generated, as these genes are used to set up the state of the - database, and Cassandra would just reject an insertion carrying an invalid one. - */ - "timestamp" -> DateTimeGene(name, onlyValid = true) - "date" -> DateGene(name, onlyValidDates = true) - "time" -> TimeGene(name, onlyValidTimes = true) - else -> throw IllegalArgumentException("Cannot handle the CQL type of column $column") - } + return builder(column.name) } private fun normalize(cqlType: String) = cqlType.trim().lowercase() - private val SUPPORTED_CQL_TYPES = setOf( - "ascii", "text", "varchar", - "tinyint", "smallint", "int", "bigint", "varint", "decimal", "float", "double", - "boolean", - "uuid", - "timestamp", "date", "time" - ) } diff --git a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraDbAction.kt b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraDbAction.kt index a8dfbe4169..eafd636b69 100644 --- a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraDbAction.kt +++ b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraDbAction.kt @@ -22,6 +22,11 @@ class CassandraDbAction( * There is exactly one gene per column, in the same order. */ val columns: List, + /** + * The genes generating the value of each of the [columns], in the same order. + * Only meant to be given when copying an existing action, so that its genes are carried over + * instead of being built anew: when not given, one gene is built per column. + */ computedGenes: List? = null ) : EnvironmentAction(listOf()) { diff --git a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraInsertBuilder.kt b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraInsertBuilder.kt index 3aa5b36161..70f63cb849 100644 --- a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraInsertBuilder.kt +++ b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraInsertBuilder.kt @@ -14,41 +14,70 @@ class CassandraInsertBuilder { private val log: Logger = LoggerFactory.getLogger(CassandraInsertBuilder::class.java) } + /** + * @param tableSchema the description of the columns of a table, as reported by the SUT driver + * @return whether an insertion that could be executed can be built for such a table, ie whether + * a value can be generated for at least one of its columns and for all of the ones composing + * its primary key + */ + fun canBuildInsertionFor(tableSchema: String): Boolean { + + val (supported, unsupported) = partitionBySupport(CassandraTableSchemaParser.parse(tableSchema)) + + return supported.isNotEmpty() && unsupported.none { isPartOfPrimaryKey(it) } + } + /** * The columns whose CQL type is not handled are left out of the insertion, as no value can be * generated for them. The resulting insertion is still worth executing, since the remaining * columns might be all that is needed, and a rejected insertion is already recorded as a failed * one instead of stopping the search. * + * That argument does not hold when no value can be generated for any column, nor when one of + * the skipped columns is part of the primary key, as Cassandra requires a full primary key in + * an INSERT: in both cases the insertion could only be rejected, so none is built. + * * Note that the genes of the returned action are not initialized yet, which is left to the * caller, as it is done for the other types of database action. + * + * @throws IllegalArgumentException if no insertion that could be executed can be built for the + * table, as verifiable beforehand with [canBuildInsertionFor] */ fun createCassandraInsertionAction(keyspace: String, table: String, tableSchema: String): CassandraDbAction { - val columns = CassandraTableSchemaParser.parse(tableSchema) + val (supported, unsupported) = partitionBySupport(CassandraTableSchemaParser.parse(tableSchema)) + + val qualifiedTableName = "$keyspace.$table" - val (supported, unsupported) = columns.partition { CassandraColumnGeneBuilder.isSupported(it) } + if (supported.isEmpty()) { + throw IllegalArgumentException("No value can be generated for any column of" + + " $qualifiedTableName: ${describe(unsupported)}") + } + + val unsupportedKeyColumns = unsupported.filter { isPartOfPrimaryKey(it) } + if (unsupportedKeyColumns.isNotEmpty()) { + throw IllegalArgumentException("No value can be generated for some of the columns composing" + + " the primary key of $qualifiedTableName: ${describe(unsupportedKeyColumns)}") + } if (unsupported.isNotEmpty()) { LoggingUtil.uniqueWarn( log, - "Cannot generate data for some columns of $keyspace.$table, as their CQL type is not handled: {}", - unsupported.joinToString(", ") { "${it.name} ${it.cqlType}" } + "Cannot generate data for some columns of a Cassandra table, as their CQL type is not handled: {}", + "$qualifiedTableName: ${describe(unsupported)}" ) - - /* - Cassandra requires a full primary key in an INSERT, so leaving out any of those - columns means the insertion is going to be rejected. - */ - if (unsupported.any { it.isPartitionKey || it.isClusteringColumn }) { - LoggingUtil.uniqueWarn( - log, - "Some of those columns are part of the primary key of {}, so the insertion will fail", - "$keyspace.$table" - ) - } } return CassandraDbAction(keyspace, table, supported).apply { forceNewTaints() } } + + /** + * @return the columns a value can be generated for (first), and the ones it cannot (second) + */ + private fun partitionBySupport(columns: List) = + columns.partition { CassandraColumnGeneBuilder.isSupported(it) } + + private fun isPartOfPrimaryKey(column: CassandraColumn) = column.isPartitionKey || column.isClusteringColumn + + private fun describe(columns: List) = columns.joinToString(", ") { "${it.name} ${it.cqlType}" } } diff --git a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraLiteralRenderer.kt b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraLiteralRenderer.kt index 4bca18145a..264c42a2a1 100644 --- a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraLiteralRenderer.kt +++ b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraLiteralRenderer.kt @@ -3,6 +3,7 @@ package org.evomaster.core.database.cassandra import org.evomaster.core.search.gene.BooleanGene import org.evomaster.core.search.gene.Gene import org.evomaster.core.search.gene.UUIDGene +import org.evomaster.core.search.gene.cassandra.CqlDurationGene import org.evomaster.core.search.gene.datetime.DateGene import org.evomaster.core.search.gene.datetime.DateTimeGene import org.evomaster.core.search.gene.datetime.TimeGene @@ -14,7 +15,7 @@ import org.evomaster.core.search.gene.string.StringGene * * This is needed because such literals are inserted verbatim into the INSERT command built on the * client side, and how a value has to be written depends on its type: text and the temporal types - * are enclosed in single quotes, whereas numbers, booleans and uuids are not. + * are enclosed in single quotes, whereas numbers, booleans, uuids and durations are not. */ object CassandraLiteralRenderer { @@ -35,7 +36,7 @@ object CassandraLiteralRenderer { return when (gene) { is StringGene, is DateGene, is TimeGene, is DateTimeGene -> quote(value) - is BooleanGene, is UUIDGene, is NumberGene<*> -> value + is BooleanGene, is UUIDGene, is NumberGene<*>, is CqlDurationGene -> value else -> throw IllegalArgumentException("Cannot render a CQL literal for a gene of type ${gene.javaClass.simpleName}") } } diff --git a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraTableSchemaParser.kt b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraTableSchemaParser.kt index 2557c68d2c..6d917f2540 100644 --- a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraTableSchemaParser.kt +++ b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraTableSchemaParser.kt @@ -36,6 +36,9 @@ object CassandraTableSchemaParser { /** * Splits on the separator between columns, ignoring the separators nested inside a type * parameter list, as a collection type is itself rendered with them, eg "map". + * + * @throws IllegalArgumentException if the type parameter lists are not balanced, as then there + * is no telling which of the separators are the ones between columns */ private fun splitColumns(tableSchema: String): List { @@ -50,6 +53,10 @@ object CassandraTableSchemaParser { current.append(c) } c == TYPE_PARAMETERS_END -> { + if (depth == 0) { + throw IllegalArgumentException("Unbalanced type parameters in the description" + + " of the columns of a Cassandra table: $tableSchema") + } depth-- current.append(c) } @@ -60,6 +67,12 @@ object CassandraTableSchemaParser { else -> current.append(c) } } + + if (depth != 0) { + throw IllegalArgumentException("Unbalanced type parameters in the description" + + " of the columns of a Cassandra table: $tableSchema") + } + columns.add(current.toString()) return columns diff --git a/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraColumnGeneBuilderTest.kt b/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraColumnGeneBuilderTest.kt index f905c74df7..30edf5a0a2 100644 --- a/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraColumnGeneBuilderTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraColumnGeneBuilderTest.kt @@ -3,6 +3,7 @@ package org.evomaster.core.database.cassandra import org.evomaster.core.search.gene.BooleanGene import org.evomaster.core.search.gene.Gene import org.evomaster.core.search.gene.UUIDGene +import org.evomaster.core.search.gene.cassandra.CqlDurationGene import org.evomaster.core.search.gene.datetime.DateGene import org.evomaster.core.search.gene.datetime.DateTimeGene import org.evomaster.core.search.gene.datetime.TimeGene @@ -82,13 +83,19 @@ class CassandraColumnGeneBuilderTest { assertEquals("firstName", gene.name) } + @Test + fun testDurationType() { + assertTrue(buildFor("duration") is CqlDurationGene) + } + /** * A counter is only writable with an UPDATE, and a timeuuid needs a value that a plain uuid - * gene would not produce, so neither can be given an arbitrary value in an insertion. + * gene would not produce, so neither can be given an arbitrary value in an insertion. For the + * other types, it is just that no gene generating a value for them has been written yet. */ @Test fun testUnsupportedTypes() { - listOf("counter", "timeuuid", "blob", "inet", "duration", "list", "frozen").forEach { + listOf("counter", "timeuuid", "blob", "inet", "list", "frozen").forEach { assertFalse(CassandraColumnGeneBuilder.isSupported(CassandraColumn("aColumn", it)), "$it should not be supported") assertThrows("no exception for $it") { buildFor(it) } } diff --git a/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraInsertBuilderTest.kt b/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraInsertBuilderTest.kt index a2d65adf0c..6841e85809 100644 --- a/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraInsertBuilderTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraInsertBuilderTest.kt @@ -3,8 +3,10 @@ package org.evomaster.core.database.cassandra import org.evomaster.core.search.gene.UUIDGene import org.evomaster.core.search.gene.string.StringGene import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows class CassandraInsertBuilderTest { @@ -57,11 +59,43 @@ class CassandraInsertBuilderTest { assertEquals(listOf("id", "name"), action.columns.map { it.name }) } + /** + * An insertion with no column at all could only be rejected, so none is built. + */ @Test - fun testTableWithNoSupportedColumn() { - val action = builder.createCassandraInsertionAction("ks", "blobs", "content blob") + fun testTableWithNoSupportedColumnIsRejected() { + assertThrows { + builder.createCassandraInsertionAction("ks", "blobs", "content blob") + } + assertFalse(builder.canBuildInsertionFor("content blob")) + } - assertTrue(action.seeTopGenes().isEmpty()) + /** + * Cassandra requires a full primary key in an INSERT, so an insertion leaving out one of the + * columns composing it could only be rejected. + */ + @Test + fun testTableWithUnsupportedPartitionKeyIsRejected() { + assertThrows { + builder.createCassandraInsertionAction("ks", "users", "id blob PARTITION KEY, name text") + } + assertFalse(builder.canBuildInsertionFor("id blob PARTITION KEY, name text")) + } + + @Test + fun testTableWithUnsupportedClusteringColumnIsRejected() { + val schema = "id uuid PARTITION KEY, at blob CLUSTERING, note text" + + assertThrows { + builder.createCassandraInsertionAction("ks", "events", schema) + } + assertFalse(builder.canBuildInsertionFor(schema)) + } + + @Test + fun testInsertionCanBeBuiltWhenOnlyRegularColumnsAreSkipped() { + assertTrue(builder.canBuildInsertionFor("id uuid PARTITION KEY, picture blob, name text")) + assertTrue(builder.canBuildInsertionFor("id uuid PARTITION KEY, name text")) } @Test diff --git a/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraLiteralRendererTest.kt b/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraLiteralRendererTest.kt index 5782cb46bc..eae3be0013 100644 --- a/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraLiteralRendererTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraLiteralRendererTest.kt @@ -3,6 +3,7 @@ package org.evomaster.core.database.cassandra import org.evomaster.core.search.gene.BooleanGene import org.evomaster.core.search.gene.ObjectGene import org.evomaster.core.search.gene.UUIDGene +import org.evomaster.core.search.gene.cassandra.CqlDurationGene import org.evomaster.core.search.gene.datetime.DateGene import org.evomaster.core.search.gene.datetime.DateTimeGene import org.evomaster.core.search.gene.datetime.TimeGene @@ -74,6 +75,24 @@ class CassandraLiteralRendererTest { } } + /** + * A duration literal is written without quotes in CQL, sign included. + */ + @Test + fun testDurationIsNotQuoted() { + val gene = CqlDurationGene( + "elapsed", + months = IntegerGene("months", 1), + days = IntegerGene("days", 2), + nanos = LongGene("nanos", 3L) + ) + + assertEquals("1mo2d3ns", CassandraLiteralRenderer.toCqlLiteral(gene)) + + gene.negative.value = true + assertEquals("-1mo2d3ns", CassandraLiteralRenderer.toCqlLiteral(gene)) + } + @Test fun testGeneWithNoCqlRepresentationIsRejected() { assertThrows { diff --git a/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraTableSchemaParserTest.kt b/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraTableSchemaParserTest.kt index f98ba68f67..feed13518f 100644 --- a/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraTableSchemaParserTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraTableSchemaParserTest.kt @@ -82,4 +82,18 @@ class CassandraTableSchemaParserTest { fun testColumnWithNoTypeIsRejected() { assertThrows { CassandraTableSchemaParser.parse("name") } } + + /** + * With unbalanced type parameters, there is no telling which of the separators are the ones + * between columns, so the description is rejected instead of being split at the wrong places. + */ + @Test + fun testUnclosedTypeParametersAreRejected() { + assertThrows { CassandraTableSchemaParser.parse("tags map { CassandraTableSchemaParser.parse("a text>, b int") } + } } From 6240011c02a85bf19a1205603d225c39f3a017b4 Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Fri, 28 Aug 2026 19:54:12 -0300 Subject: [PATCH 6/8] Add CqlDurationGene --- .../insertions/CassandraScriptRunnerTest.java | 22 +++- .../search/gene/cassandra/CqlDurationGene.kt | 107 ++++++++++++++++++ .../core/search/gene/GeneNumberOfGenesTest.kt | 2 +- .../core/search/gene/GeneSamplerForTests.kt | 8 ++ .../gene/cassandra/CqlDurationGeneTest.kt | 73 ++++++++++++ 5 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 core/src/main/kotlin/org/evomaster/core/search/gene/cassandra/CqlDurationGene.kt create mode 100644 core/src/test/kotlin/org/evomaster/core/search/gene/cassandra/CqlDurationGeneTest.kt diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/insertions/CassandraScriptRunnerTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/insertions/CassandraScriptRunnerTest.java index e471771b99..8abfdd7561 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/insertions/CassandraScriptRunnerTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/insertions/CassandraScriptRunnerTest.java @@ -46,7 +46,7 @@ public static void initClass() { connection.execute("CREATE KEYSPACE IF NOT EXISTS " + KEYSPACE + " WITH replication = {'class':'SimpleStrategy','replication_factor':1}"); connection.execute("CREATE TABLE IF NOT EXISTS " + KEYSPACE + "." + TABLE + - " (id int PRIMARY KEY, name text)"); + " (id int PRIMARY KEY, name text, elapsed duration)"); } @AfterAll @@ -77,6 +77,26 @@ public void testInsert() { assertTrue(connection.execute("SELECT * FROM " + KEYSPACE + "." + TABLE).iterator().hasNext()); } + /** + * A duration is written as a bare literal, ie not enclosed in quotes the way a text is, and it + * carries at most one leading sign, applying to the whole value. This is what + * CassandraLiteralRenderer in the core module relies on when rendering the value of a + * CqlDurationGene, so it is checked here against a real Cassandra. + */ + @Test + public void testInsertDuration() { + List insertions = CassandraDsl.cassandra() + .insertInto(KEYSPACE, TABLE).d("id", "1").d("elapsed", "1mo2d3ns") + .and().insertInto(KEYSPACE, TABLE).d("id", "2").d("elapsed", "-1mo2d3ns") + .dtos(); + + CassandraInsertionResultsDto resultsDto = CassandraScriptRunner.executeInsert(connection, insertions); + + assertTrue(resultsDto.executionResults.get(0)); + assertTrue(resultsDto.executionResults.get(1)); + assertEquals(2, connection.execute("SELECT * FROM " + KEYSPACE + "." + TABLE).all().size()); + } + @Test public void testInsertionFailureDoesNotStopFollowingInsertions() { diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/cassandra/CqlDurationGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/cassandra/CqlDurationGene.kt new file mode 100644 index 0000000000..9b588d47a6 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/cassandra/CqlDurationGene.kt @@ -0,0 +1,107 @@ +package org.evomaster.core.search.gene.cassandra + +import org.evomaster.core.output.OutputFormat +import org.evomaster.core.search.gene.BooleanGene +import org.evomaster.core.search.gene.Gene +import org.evomaster.core.search.gene.numeric.IntegerGene +import org.evomaster.core.search.gene.numeric.LongGene +import org.evomaster.core.search.gene.root.CompositeFixedGene +import org.evomaster.core.search.gene.utils.GeneUtils +import org.evomaster.core.search.service.Randomness +import org.evomaster.core.search.service.mutator.genemutation.AdditionalGeneMutationInfo +import org.evomaster.core.search.service.mutator.genemutation.SubsetGeneMutationSelectionStrategy + +/** + * A value of the Cassandra "duration" type, which is composed of a number of months, a number of + * days, and a number of nanoseconds, kept apart from each other rather than reduced to a single + * amount of time, as the length of a month and of a day both depend on the date they are counted + * from. + * + * The three amounts share a single sign, instead of having one each: a duration literal is written + * with at most one leading "-", which applies to the whole value, so a duration mixing signs has no + * representation in CQL. + * + * Note that the representation is not unique, as all the amounts being zero and [negative] being + * true renders "-0mo0d0ns", ie the same value as the positive zero duration spelled differently. + */ +class CqlDurationGene( + name: String, + val months: IntegerGene = IntegerGene("months", min = 0), + val days: IntegerGene = IntegerGene("days", min = 0), + val nanos: LongGene = LongGene("nanos", min = 0), + /** + * Whether the duration is negative, ie the sign shared by the three amounts it is composed of. + * Explicitly defaulted to false, as [BooleanGene] defaults to true. + */ + val negative: BooleanGene = BooleanGene("negative", false) +) : CompositeFixedGene(name, mutableListOf(months, days, nanos, negative)) { + + override fun copyContent(): Gene = CqlDurationGene( + name, + months.copy() as IntegerGene, + days.copy() as IntegerGene, + nanos.copy() as LongGene, + negative.copy() as BooleanGene + ) + + override fun checkForLocallyValidIgnoringChildren(): Boolean { + return true + } + + override fun randomize(randomness: Randomness, tryToForceNewValue: Boolean) { + months.randomize(randomness, tryToForceNewValue) + days.randomize(randomness, tryToForceNewValue) + nanos.randomize(randomness, tryToForceNewValue) + negative.randomize(randomness, tryToForceNewValue) + } + + override fun getValueAsPrintableString( + previousGenes: List, + mode: GeneUtils.EscapeMode?, + targetFormat: OutputFormat?, + extraCheck: Boolean + ): String { + return "\"${getValueAsRawString()}\"" + } + + /** + * @return the duration as it is written in a CQL statement, in the standard Cassandra format. + * All three amounts are always written, so that the literal is well formed even when they are + * all zero. + */ + override fun getValueAsRawString(): String { + val sign = if (negative.value) "-" else "" + return "$sign${months.value}mo${days.value}d${nanos.value}ns" + } + + override fun unsafeCopyValueFrom(other: Gene): Boolean { + if (other !is CqlDurationGene) { + return false + } + + return this.months.unsafeCopyValueFrom(other.months) + && this.days.unsafeCopyValueFrom(other.days) + && this.nanos.unsafeCopyValueFrom(other.nanos) + && this.negative.unsafeCopyValueFrom(other.negative) + } + + override fun containsSameValueAs(other: Gene): Boolean { + if (other !is CqlDurationGene) { + return false + } + + return this.months.containsSameValueAs(other.months) + && this.days.containsSameValueAs(other.days) + && this.nanos.containsSameValueAs(other.nanos) + && this.negative.containsSameValueAs(other.negative) + } + + override fun customShouldApplyShallowMutation( + randomness: Randomness, + selectionStrategy: SubsetGeneMutationSelectionStrategy, + enableAdaptiveGeneMutation: Boolean, + additionalGeneMutationInfo: AdditionalGeneMutationInfo? + ): Boolean { + return false + } +} \ No newline at end of file diff --git a/core/src/test/kotlin/org/evomaster/core/search/gene/GeneNumberOfGenesTest.kt b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneNumberOfGenesTest.kt index 7c3a1823fc..a090467d41 100644 --- a/core/src/test/kotlin/org/evomaster/core/search/gene/GeneNumberOfGenesTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneNumberOfGenesTest.kt @@ -13,7 +13,7 @@ class GeneNumberOfGenesTest : AbstractGeneTest() { This number should not change, unless you explicitly add/remove any gene. if so, update this number accordingly */ - assertEquals(95, geneClasses.size) + assertEquals(96, geneClasses.size) } } diff --git a/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt index 280fa1b628..4528d42cd5 100644 --- a/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt +++ b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt @@ -2,6 +2,7 @@ package org.evomaster.core.search.gene import org.evomaster.client.java.instrumentation.shared.TaintInputName import org.evomaster.core.parser.RegexType +import org.evomaster.core.search.gene.cassandra.CqlDurationGene import org.evomaster.core.search.gene.collection.* import org.evomaster.core.search.gene.datetime.* import org.evomaster.core.search.gene.interfaces.ComparableGene @@ -186,6 +187,9 @@ object GeneSamplerForTests { // Mongo genes ObjectIdGene::class -> sampleMongoObjectIdGene(rand) as T + // Cassandra genes + CqlDurationGene::class -> sampleCqlDurationGene(rand) as T + // JSON Patch genes JsonPatchDocumentGene::class -> sampleJsonPatchDocumentGene(rand) as T JsonPatchPathOnlyGene::class -> sampleJsonPatchPathOnlyGene(rand) as T @@ -419,6 +423,10 @@ object GeneSamplerForTests { return ObjectIdGene("rand ObjectIdGene ${rand.nextInt()}") } + private fun sampleCqlDurationGene(rand: Randomness): CqlDurationGene { + return CqlDurationGene("rand CqlDurationGene ${rand.nextInt()}") + } + fun sampleBackReferenceRxGene(rand: Randomness): BackReferenceRxGene { val captureGroup = sampleDisjunctionListRxGene(rand) // as we do not allow to mutate the inner captureGroup gene using the backref gene we must first initialize it diff --git a/core/src/test/kotlin/org/evomaster/core/search/gene/cassandra/CqlDurationGeneTest.kt b/core/src/test/kotlin/org/evomaster/core/search/gene/cassandra/CqlDurationGeneTest.kt new file mode 100644 index 0000000000..4c00d4fd66 --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/search/gene/cassandra/CqlDurationGeneTest.kt @@ -0,0 +1,73 @@ +package org.evomaster.core.search.gene.cassandra + +import org.evomaster.core.search.gene.numeric.IntegerGene +import org.evomaster.core.search.gene.numeric.LongGene +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class CqlDurationGeneTest { + + private fun duration(months: Int, days: Int, nanos: Long, negative: Boolean = false) = + CqlDurationGene( + "elapsed", + months = IntegerGene("months", months), + days = IntegerGene("days", days), + nanos = LongGene("nanos", nanos) + ).apply { this.negative.value = negative } + + @Test + fun testValueIsRenderedWithTheThreeUnits() { + assertEquals("1mo2d3ns", duration(1, 2, 3L).getValueAsRawString()) + } + + /** + * All the amounts are written even when zero, so that the literal is never empty. + */ + @Test + fun testZeroDuration() { + assertEquals("0mo0d0ns", duration(0, 0, 0L).getValueAsRawString()) + } + + /** + * A duration literal carries at most one sign, applying to the whole value, as a duration + * mixing signs cannot be written in CQL. + */ + @Test + fun testNegativeDurationHasASingleLeadingSign() { + assertEquals("-1mo2d3ns", duration(1, 2, 3L, negative = true).getValueAsRawString()) + } + + @Test + fun testDurationIsPositiveByDefault() { + assertFalse(CqlDurationGene("elapsed").negative.value) + } + + @Test + fun testCopyKeepsAllTheComponents() { + val gene = duration(1, 2, 3L, negative = true) + val copy = gene.copy() as CqlDurationGene + + assertEquals(gene.getValueAsRawString(), copy.getValueAsRawString()) + assertTrue(gene.containsSameValueAs(copy)) + } + + @Test + fun testDurationsDifferingInOneComponentAreNotTheSame() { + val gene = duration(1, 2, 3L) + + assertFalse(gene.containsSameValueAs(duration(9, 2, 3L))) + assertFalse(gene.containsSameValueAs(duration(1, 9, 3L))) + assertFalse(gene.containsSameValueAs(duration(1, 2, 9L))) + assertFalse(gene.containsSameValueAs(duration(1, 2, 3L, negative = true))) + } + + @Test + fun testCopyValueFrom() { + val gene = CqlDurationGene("elapsed") + + assertTrue(gene.copyValueFrom(duration(1, 2, 3L, negative = true))) + assertEquals("-1mo2d3ns", gene.getValueAsRawString()) + } +} \ No newline at end of file From 81ada113123fdf30370912e25ece2ce932407a58 Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Fri, 4 Sep 2026 21:33:16 -0300 Subject: [PATCH 7/8] Address PR comments - Add proper schema DTOs to use in insertionBuilder - Change attribute name - Replace strings with constants --- .../cassandra/CassandraColumnDto.java | 70 +++++++++++ .../cassandra/CassandraTableSchemaDto.java | 57 +++++++++ .../execution/CassandraFailedQuery.java | 33 +++++- .../db/cassandra/CassandraHandler.java | 38 +++--- .../db/cassandra/CassandraHandlerTest.java | 23 +++- .../database/cassandra/CassandraColumn.kt | 22 +++- .../cassandra/CassandraColumnGeneBuilder.kt | 52 ++++++--- .../cassandra/CassandraInsertBuilder.kt | 30 +++-- .../cassandra/CassandraTableSchemaParser.kt | 110 ------------------ .../search/gene/cassandra/CqlDurationGene.kt | 16 +-- .../cassandra/CassandraInsertBuilderTest.kt | 70 +++++++---- .../cassandra/CassandraLiteralRendererTest.kt | 2 +- .../CassandraTableSchemaParserTest.kt | 99 ---------------- .../gene/cassandra/CqlDurationGeneTest.kt | 4 +- 14 files changed, 321 insertions(+), 305 deletions(-) create mode 100644 client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/cassandra/CassandraColumnDto.java create mode 100644 client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/cassandra/CassandraTableSchemaDto.java delete mode 100644 core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraTableSchemaParser.kt delete mode 100644 core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraTableSchemaParserTest.kt diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/cassandra/CassandraColumnDto.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/cassandra/CassandraColumnDto.java new file mode 100644 index 0000000000..665957290d --- /dev/null +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/cassandra/CassandraColumnDto.java @@ -0,0 +1,70 @@ +package org.evomaster.client.java.controller.api.dto.database.cassandra; + +/** + * A single column of a Cassandra table, as read from the driver's own metadata on the SUT side and + * reported to the core process. + */ +public class CassandraColumnDto { + + private String name; + + /** + * The type of the column, as named in CQL, eg "text", "int", "map<text, int>". + */ + private String cqlType; + + /** + * Whether this column is part of the partition key of the table. + */ + private boolean partitionKey; + + /** + * Whether this column is one of the clustering columns of the table. + */ + private boolean clusteringColumn; + + /** + * Needed to deserialize the DTO, as it is sent over HTTP. + */ + public CassandraColumnDto() { + } + + public CassandraColumnDto(String name, String cqlType, boolean partitionKey, boolean clusteringColumn) { + this.name = name; + this.cqlType = cqlType; + this.partitionKey = partitionKey; + this.clusteringColumn = clusteringColumn; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCqlType() { + return cqlType; + } + + public void setCqlType(String cqlType) { + this.cqlType = cqlType; + } + + public boolean isPartitionKey() { + return partitionKey; + } + + public void setPartitionKey(boolean partitionKey) { + this.partitionKey = partitionKey; + } + + public boolean isClusteringColumn() { + return clusteringColumn; + } + + public void setClusteringColumn(boolean clusteringColumn) { + this.clusteringColumn = clusteringColumn; + } +} \ No newline at end of file diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/cassandra/CassandraTableSchemaDto.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/cassandra/CassandraTableSchemaDto.java new file mode 100644 index 0000000000..4a0160e783 --- /dev/null +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/cassandra/CassandraTableSchemaDto.java @@ -0,0 +1,57 @@ +package org.evomaster.client.java.controller.api.dto.database.cassandra; + +import java.util.ArrayList; +import java.util.List; + +/** + * The shape of a Cassandra table, ie the columns a row of it is composed of, as read from the + * driver's own metadata on the SUT side. It is what the core process bases the data it generates + * for such a table on. + */ +public class CassandraTableSchemaDto { + + private String keyspaceName; + + private String tableName; + + /** + * All the columns of the table, in the order the driver reports them. + */ + private List columns = new ArrayList<>(); + + /** + * Needed to deserialize the DTO, as it is sent over HTTP. + */ + public CassandraTableSchemaDto() { + } + + public CassandraTableSchemaDto(String keyspaceName, String tableName, List columns) { + this.keyspaceName = keyspaceName; + this.tableName = tableName; + this.columns = columns; + } + + public String getKeyspaceName() { + return keyspaceName; + } + + public void setKeyspaceName(String keyspaceName) { + this.keyspaceName = keyspaceName; + } + + public String getTableName() { + return tableName; + } + + public void setTableName(String tableName) { + this.tableName = tableName; + } + + public List getColumns() { + return columns; + } + + public void setColumns(List columns) { + this.columns = columns; + } +} \ No newline at end of file diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/execution/CassandraFailedQuery.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/execution/CassandraFailedQuery.java index 57e4f84438..555d56bcb0 100644 --- a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/execution/CassandraFailedQuery.java +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/execution/CassandraFailedQuery.java @@ -1,5 +1,7 @@ package org.evomaster.client.java.controller.api.dto.database.execution; +import org.evomaster.client.java.controller.api.dto.database.cassandra.CassandraTableSchemaDto; + import java.util.Objects; /** @@ -10,17 +12,24 @@ public class CassandraFailedQuery { /** * The keyspace the table belongs to. */ - private final String keyspaceName; + private String keyspaceName; /** * The table the query targeted. */ - private final String tableName; + private String tableName; + /** + * The shape of the table's rows, null when it was never captured, ie when no query referencing + * the table was intercepted while the schema of its tables was being tracked. + */ + private CassandraTableSchemaDto tableSchema; + /** - * The schema of the table's rows, if known. + * Needed to deserialize the DTO, as it is sent over HTTP. */ - private String tableSchema; + public CassandraFailedQuery() { + } - public CassandraFailedQuery(String keyspaceName, String tableName, String tableSchema) { + public CassandraFailedQuery(String keyspaceName, String tableName, CassandraTableSchemaDto tableSchema) { this.keyspaceName = Objects.requireNonNull(keyspaceName); this.tableName = Objects.requireNonNull(tableName); this.tableSchema = tableSchema; @@ -30,11 +39,23 @@ public String getKeyspaceName() { return keyspaceName; } + public void setKeyspaceName(String keyspaceName) { + this.keyspaceName = keyspaceName; + } + public String getTableName() { return tableName; } - public String getTableSchema() { + public void setTableName(String tableName) { + this.tableName = tableName; + } + + public CassandraTableSchemaDto getTableSchema() { return tableSchema; } + + public void setTableSchema(CassandraTableSchemaDto tableSchema) { + this.tableSchema = tableSchema; + } } \ No newline at end of file diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/db/cassandra/CassandraHandler.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/db/cassandra/CassandraHandler.java index cfc398fde9..a503dab118 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/db/cassandra/CassandraHandler.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/db/cassandra/CassandraHandler.java @@ -1,5 +1,7 @@ package org.evomaster.client.java.controller.internal.db.cassandra; +import org.evomaster.client.java.controller.api.dto.database.cassandra.CassandraColumnDto; +import org.evomaster.client.java.controller.api.dto.database.cassandra.CassandraTableSchemaDto; import org.evomaster.client.java.controller.api.dto.database.execution.CassandraExecutionsDto; import org.evomaster.client.java.controller.api.dto.database.execution.CassandraFailedQuery; import org.evomaster.client.java.controller.cassandra.calculator.CassandraHeuristicsCalculator; @@ -44,13 +46,6 @@ public class CassandraHandler { private static final String METHOD_AS_INTERNAL = "asInternal"; private static final String METHOD_GET_OBJECT = "getObject"; - /* - Constants used during schema description - */ - private static final String PARTITION_KEY_COLUMN_SUFFIX = " PARTITION KEY"; - private static final String CLUSTERING_COLUMN_SUFFIX = " CLUSTERING"; - private static final char COLUMN_NAME_TYPE_SEPARATOR = ' '; - private static final String SELECT_ALL_PREFIX = "SELECT * FROM "; private static final char KEYSPACE_TABLE_SEPARATOR = '.'; @@ -185,32 +180,27 @@ public CassandraExecutionsDto getExecutionDto() { private CassandraFailedQuery extractRelevantInfo(ExecutedCqlCommand info) { CassandraTableMetadata schema = tableSchemas.get(new TableKey(info.getKeyspaceName(), info.getTableName())); - String tableSchema = schema != null ? describeTableSchema(schema) : null; + CassandraTableSchemaDto tableSchema = schema != null ? toDto(schema) : null; return new CassandraFailedQuery(info.getKeyspaceName(), info.getTableName(), tableSchema); } /** - * Renders a table's columns as {@code name type [PARTITION KEY|CLUSTERING]} entries, so the - * schema can be attached to a {@link CassandraFailedQuery} (a plain string, to keep + * Converts a captured table schema into the DTO it is reported with, which is what keeps * controller-api free of a dependency on the instrumentation module's - * {@link CassandraTableMetadata}). + * {@link CassandraTableMetadata}. */ - private static String describeTableSchema(CassandraTableMetadata schema) { - return schema.getColumns().stream() - .map(CassandraHandler::describeColumn) - .collect(Collectors.joining(", ")); + private static CassandraTableSchemaDto toDto(CassandraTableMetadata schema) { + List columns = schema.getColumns().stream() + .map(CassandraHandler::toDto) + .collect(Collectors.toList()); + + return new CassandraTableSchemaDto(schema.getKeyspaceName(), schema.getTableName(), columns); } - private static String describeColumn(CassandraColumnMetadata column) { - StringBuilder description = new StringBuilder(column.getName()).append(COLUMN_NAME_TYPE_SEPARATOR).append(column.getCqlType()); - if (column.isPartitionKey()) { - description.append(PARTITION_KEY_COLUMN_SUFFIX); - } - if (column.isClusteringColumn()) { - description.append(CLUSTERING_COLUMN_SUFFIX); - } - return description.toString(); + private static CassandraColumnDto toDto(CassandraColumnMetadata column) { + return new CassandraColumnDto(column.getName(), column.getCqlType(), + column.isPartitionKey(), column.isClusteringColumn()); } private CqlDistanceWithMetrics computeQueryDistance(ExecutedCqlCommand info) { diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/cassandra/CassandraHandlerTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/cassandra/CassandraHandlerTest.java index 806e6b2364..f8355c11bd 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/cassandra/CassandraHandlerTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/cassandra/CassandraHandlerTest.java @@ -2,6 +2,8 @@ import com.datastax.oss.driver.api.core.CqlSession; import com.datastax.oss.driver.api.core.cql.ResultSet; +import org.evomaster.client.java.controller.api.dto.database.cassandra.CassandraColumnDto; +import org.evomaster.client.java.controller.api.dto.database.cassandra.CassandraTableSchemaDto; import org.evomaster.client.java.controller.api.dto.database.execution.CassandraExecutionsDto; import org.evomaster.client.java.instrumentation.ExecutedCqlCommand; import org.evomaster.client.java.instrumentation.cassandra.CassandraColumnMetadata; @@ -81,6 +83,23 @@ private static CassandraTableMetadata tableSchema(String tableName) { Collections.singletonList(new CassandraColumnMetadata("id", "int", true, false))); } + /** + * Asserts that the reported schema is the one {@link #tableSchema(String)} captured, ie that + * every part of the captured metadata reached the DTO. + */ + private void assertReportedSchemaOfTable(CassandraTableSchemaDto reported) { + assertNotNull(reported); + assertEquals(KEYSPACE, reported.getKeyspaceName()); + assertEquals(TABLE, reported.getTableName()); + assertEquals(1, reported.getColumns().size()); + + CassandraColumnDto column = reported.getColumns().get(0); + assertEquals("id", column.getName()); + assertEquals("int", column.getCqlType()); + assertTrue(column.isPartitionKey()); + assertFalse(column.isClusteringColumn()); + } + @Test public void testSelectDistance_zeroWhenRowMatches() { session.execute("INSERT INTO " + KEYSPACE + "." + TABLE + " (id, age, name) VALUES (1, 30, 'John Doe')"); @@ -168,7 +187,7 @@ public void testEmptyTable_recordedAsFailedQueryWithSchema() { assertEquals(1, dto.failedQueries.size()); assertEquals(KEYSPACE, dto.failedQueries.get(0).getKeyspaceName()); assertEquals(TABLE, dto.failedQueries.get(0).getTableName()); - assertEquals("id int PARTITION KEY", dto.failedQueries.get(0).getTableSchema()); + assertReportedSchemaOfTable(dto.failedQueries.get(0).getTableSchema()); } /** @@ -226,7 +245,7 @@ public void testReset_preservesTableSchema() { CassandraExecutionsDto dto = handler.getExecutionDto(); assertEquals(1, dto.failedQueries.size()); // schema was captured before reset() and must still be attached afterwards - assertEquals("id int PARTITION KEY", dto.failedQueries.get(0).getTableSchema()); + assertReportedSchemaOfTable(dto.failedQueries.get(0).getTableSchema()); } @Test diff --git a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraColumn.kt b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraColumn.kt index 8f34534ade..55f8fd6c32 100644 --- a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraColumn.kt +++ b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraColumn.kt @@ -1,8 +1,10 @@ package org.evomaster.core.database.cassandra +import org.evomaster.client.java.controller.api.dto.database.cassandra.CassandraColumnDto + /** - * A single column of a Cassandra table, as recovered from the schema description string carried by - * a failed CQL query reported by the SUT driver. + * A single column of a Cassandra table, as reported by the SUT driver in the schema of the table a + * failed CQL query targeted. */ data class CassandraColumn( @@ -22,4 +24,18 @@ data class CassandraColumn( * Whether this column is one of the table's clustering columns. */ val isClusteringColumn: Boolean = false -) +) { + + companion object { + + /** + * @param dto the column of a table, as reported by the SUT driver + */ + fun fromDto(dto: CassandraColumnDto) = CassandraColumn( + name = dto.name, + cqlType = dto.cqlType, + isPartitionKey = dto.isPartitionKey, + isClusteringColumn = dto.isClusteringColumn + ) + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraColumnGeneBuilder.kt b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraColumnGeneBuilder.kt index 3f9c897b25..65659e3742 100644 --- a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraColumnGeneBuilder.kt +++ b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraColumnGeneBuilder.kt @@ -22,33 +22,51 @@ import org.evomaster.core.search.gene.string.StringGene */ object CassandraColumnGeneBuilder { + private const val ASCII_TYPE = "ascii" + private const val TEXT_TYPE = "text" + private const val VARCHAR_TYPE = "varchar" + private const val TINYINT_TYPE = "tinyint" + private const val SMALLINT_TYPE = "smallint" + private const val INT_TYPE = "int" + private const val BIGINT_TYPE = "bigint" + private const val VARINT_TYPE = "varint" + private const val DECIMAL_TYPE = "decimal" + private const val FLOAT_TYPE = "float" + private const val DOUBLE_TYPE = "double" + private const val BOOLEAN_TYPE = "boolean" + private const val UUID_TYPE = "uuid" + private const val TIMESTAMP_TYPE = "timestamp" + private const val DATE_TYPE = "date" + private const val TIME_TYPE = "time" + private const val DURATION_TYPE = "duration" + /** * How the gene generating the value of a column is built, for each of the CQL types handled * here, keyed by the normalized name of the type. Being the single place where such types are * enumerated, it is also what [isSupported] answers from, so that the two cannot disagree. */ private val GENE_BUILDERS: Map Gene> = mapOf( - "ascii" to { name -> StringGene(name) }, - "text" to { name -> StringGene(name) }, - "varchar" to { name -> StringGene(name) }, - "tinyint" to { name -> IntegerGene(name, min = Byte.MIN_VALUE.toInt(), max = Byte.MAX_VALUE.toInt()) }, - "smallint" to { name -> IntegerGene(name, min = Short.MIN_VALUE.toInt(), max = Short.MAX_VALUE.toInt()) }, - "int" to { name -> IntegerGene(name) }, - "bigint" to { name -> LongGene(name) }, - "varint" to { name -> BigIntegerGene(name) }, - "decimal" to { name -> BigDecimalGene(name) }, - "float" to { name -> FloatGene(name) }, - "double" to { name -> DoubleGene(name) }, - "boolean" to { name -> BooleanGene(name) }, - "uuid" to { name -> UUIDGene(name) }, + ASCII_TYPE to { name -> StringGene(name) }, + TEXT_TYPE to { name -> StringGene(name) }, + VARCHAR_TYPE to { name -> StringGene(name) }, + TINYINT_TYPE to { name -> IntegerGene(name, min = Byte.MIN_VALUE.toInt(), max = Byte.MAX_VALUE.toInt()) }, + SMALLINT_TYPE to { name -> IntegerGene(name, min = Short.MIN_VALUE.toInt(), max = Short.MAX_VALUE.toInt()) }, + INT_TYPE to { name -> IntegerGene(name) }, + BIGINT_TYPE to { name -> LongGene(name) }, + VARINT_TYPE to { name -> BigIntegerGene(name) }, + DECIMAL_TYPE to { name -> BigDecimalGene(name) }, + FLOAT_TYPE to { name -> FloatGene(name) }, + DOUBLE_TYPE to { name -> DoubleGene(name) }, + BOOLEAN_TYPE to { name -> BooleanGene(name) }, + UUID_TYPE to { name -> UUIDGene(name) }, /* Only valid values are generated, as these genes are used to set up the state of the database, and Cassandra would just reject an insertion carrying an invalid one. */ - "timestamp" to { name -> DateTimeGene(name, onlyValid = true) }, - "date" to { name -> DateGene(name, onlyValidDates = true) }, - "time" to { name -> TimeGene(name, onlyValidTimes = true) }, - "duration" to { name -> CqlDurationGene(name) } + TIMESTAMP_TYPE to { name -> DateTimeGene(name, onlyValid = true) }, + DATE_TYPE to { name -> DateGene(name, onlyValidDates = true) }, + TIME_TYPE to { name -> TimeGene(name, onlyValidTimes = true) }, + DURATION_TYPE to { name -> CqlDurationGene(name) } ) /** diff --git a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraInsertBuilder.kt b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraInsertBuilder.kt index 70f63cb849..8379a57300 100644 --- a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraInsertBuilder.kt +++ b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraInsertBuilder.kt @@ -1,12 +1,13 @@ package org.evomaster.core.database.cassandra +import org.evomaster.client.java.controller.api.dto.database.cassandra.CassandraTableSchemaDto import org.evomaster.core.logging.LoggingUtil import org.slf4j.Logger import org.slf4j.LoggerFactory /** - * Builds the action inserting a row into a Cassandra table, based on the description of the - * columns of that table reported by the SUT driver. + * Builds the action inserting a row into a Cassandra table, based on the schema of that table + * reported by the SUT driver. */ class CassandraInsertBuilder { @@ -15,14 +16,14 @@ class CassandraInsertBuilder { } /** - * @param tableSchema the description of the columns of a table, as reported by the SUT driver + * @param tableSchema the schema of a table, as reported by the SUT driver * @return whether an insertion that could be executed can be built for such a table, ie whether * a value can be generated for at least one of its columns and for all of the ones composing * its primary key */ - fun canBuildInsertionFor(tableSchema: String): Boolean { + fun canBuildInsertionFor(tableSchema: CassandraTableSchemaDto): Boolean { - val (supported, unsupported) = partitionBySupport(CassandraTableSchemaParser.parse(tableSchema)) + val (supported, unsupported) = partitionBySupport(tableSchema) return supported.isNotEmpty() && unsupported.none { isPartOfPrimaryKey(it) } } @@ -40,14 +41,16 @@ class CassandraInsertBuilder { * Note that the genes of the returned action are not initialized yet, which is left to the * caller, as it is done for the other types of database action. * + * @param tableSchema the schema of the table to insert a row into, which is also what says + * where such a table is, ie its keyspace and its name * @throws IllegalArgumentException if no insertion that could be executed can be built for the * table, as verifiable beforehand with [canBuildInsertionFor] */ - fun createCassandraInsertionAction(keyspace: String, table: String, tableSchema: String): CassandraDbAction { + fun createCassandraInsertionAction(tableSchema: CassandraTableSchemaDto): CassandraDbAction { - val (supported, unsupported) = partitionBySupport(CassandraTableSchemaParser.parse(tableSchema)) + val (supported, unsupported) = partitionBySupport(tableSchema) - val qualifiedTableName = "$keyspace.$table" + val qualifiedTableName = "${tableSchema.keyspaceName}.${tableSchema.tableName}" if (supported.isEmpty()) { throw IllegalArgumentException("No value can be generated for any column of" + @@ -68,16 +71,19 @@ class CassandraInsertBuilder { ) } - return CassandraDbAction(keyspace, table, supported).apply { forceNewTaints() } + return CassandraDbAction(tableSchema.keyspaceName, tableSchema.tableName, supported) + .apply { forceNewTaints() } } /** * @return the columns a value can be generated for (first), and the ones it cannot (second) */ - private fun partitionBySupport(columns: List) = - columns.partition { CassandraColumnGeneBuilder.isSupported(it) } + private fun partitionBySupport(tableSchema: CassandraTableSchemaDto) = + tableSchema.columns + .map { CassandraColumn.fromDto(it) } + .partition { CassandraColumnGeneBuilder.isSupported(it) } private fun isPartOfPrimaryKey(column: CassandraColumn) = column.isPartitionKey || column.isClusteringColumn private fun describe(columns: List) = columns.joinToString(", ") { "${it.name} ${it.cqlType}" } -} +} \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraTableSchemaParser.kt b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraTableSchemaParser.kt deleted file mode 100644 index 6d917f2540..0000000000 --- a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraTableSchemaParser.kt +++ /dev/null @@ -1,110 +0,0 @@ -package org.evomaster.core.database.cassandra - -/** - * Recovers the columns of a Cassandra table from the flat schema description string reported by the - * SUT driver, ie the inverse of how [CassandraColumn]s are rendered on the client side, where each - * column becomes "name type" optionally followed by a " PARTITION KEY" and/or " CLUSTERING" marker, - * and columns are joined with ", ". - */ -object CassandraTableSchemaParser { - - private const val COLUMN_SEPARATOR = ',' - - private const val COLUMN_NAME_TYPE_SEPARATOR = ' ' - - private const val PARTITION_KEY_COLUMN_SUFFIX = " PARTITION KEY" - - private const val CLUSTERING_COLUMN_SUFFIX = " CLUSTERING" - - private const val TYPE_PARAMETERS_START = '<' - - private const val TYPE_PARAMETERS_END = '>' - - /** - * @param tableSchema the description of all the columns of a table, as reported by the SUT driver - * @return the columns described in [tableSchema], in the same order - * @throws IllegalArgumentException if any of the described columns is malformed - */ - fun parse(tableSchema: String): List { - - return splitColumns(tableSchema) - .map { it.trim() } - .filter { it.isNotEmpty() } - .map { parseColumn(it) } - } - - /** - * Splits on the separator between columns, ignoring the separators nested inside a type - * parameter list, as a collection type is itself rendered with them, eg "map". - * - * @throws IllegalArgumentException if the type parameter lists are not balanced, as then there - * is no telling which of the separators are the ones between columns - */ - private fun splitColumns(tableSchema: String): List { - - val columns = mutableListOf() - val current = StringBuilder() - var depth = 0 - - for (c in tableSchema) { - when { - c == TYPE_PARAMETERS_START -> { - depth++ - current.append(c) - } - c == TYPE_PARAMETERS_END -> { - if (depth == 0) { - throw IllegalArgumentException("Unbalanced type parameters in the description" + - " of the columns of a Cassandra table: $tableSchema") - } - depth-- - current.append(c) - } - c == COLUMN_SEPARATOR && depth == 0 -> { - columns.add(current.toString()) - current.clear() - } - else -> current.append(c) - } - } - - if (depth != 0) { - throw IllegalArgumentException("Unbalanced type parameters in the description" + - " of the columns of a Cassandra table: $tableSchema") - } - - columns.add(current.toString()) - - return columns - } - - private fun parseColumn(description: String): CassandraColumn { - - var remainder = description - - /* - The two markers are appended in this order, so they have to be peeled off in reverse. - Both can in principle be present, as they are rendered independently of each other. - */ - val isClusteringColumn = remainder.endsWith(CLUSTERING_COLUMN_SUFFIX) - if (isClusteringColumn) { - remainder = remainder.removeSuffix(CLUSTERING_COLUMN_SUFFIX) - } - val isPartitionKey = remainder.endsWith(PARTITION_KEY_COLUMN_SUFFIX) - if (isPartitionKey) { - remainder = remainder.removeSuffix(PARTITION_KEY_COLUMN_SUFFIX) - } - - val separatorIndex = remainder.indexOf(COLUMN_NAME_TYPE_SEPARATOR) - if (separatorIndex <= 0 || separatorIndex == remainder.length - 1) { - throw IllegalArgumentException("Malformed description of a Cassandra column: $description") - } - - return CassandraColumn( - name = remainder.substring(0, separatorIndex), - cqlType = remainder.substring(separatorIndex + 1), - isPartitionKey = isPartitionKey, - isClusteringColumn = isClusteringColumn - ) - } -} diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/cassandra/CqlDurationGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/cassandra/CqlDurationGene.kt index 9b588d47a6..ec743b4c06 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/cassandra/CqlDurationGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/cassandra/CqlDurationGene.kt @@ -21,7 +21,7 @@ import org.evomaster.core.search.service.mutator.genemutation.SubsetGeneMutation * with at most one leading "-", which applies to the whole value, so a duration mixing signs has no * representation in CQL. * - * Note that the representation is not unique, as all the amounts being zero and [negative] being + * Note that the representation is not unique, as all the amounts being zero and [isNegative] being * true renders "-0mo0d0ns", ie the same value as the positive zero duration spelled differently. */ class CqlDurationGene( @@ -33,15 +33,15 @@ class CqlDurationGene( * Whether the duration is negative, ie the sign shared by the three amounts it is composed of. * Explicitly defaulted to false, as [BooleanGene] defaults to true. */ - val negative: BooleanGene = BooleanGene("negative", false) -) : CompositeFixedGene(name, mutableListOf(months, days, nanos, negative)) { + val isNegative: BooleanGene = BooleanGene("negative", false) +) : CompositeFixedGene(name, mutableListOf(months, days, nanos, isNegative)) { override fun copyContent(): Gene = CqlDurationGene( name, months.copy() as IntegerGene, days.copy() as IntegerGene, nanos.copy() as LongGene, - negative.copy() as BooleanGene + isNegative.copy() as BooleanGene ) override fun checkForLocallyValidIgnoringChildren(): Boolean { @@ -52,7 +52,7 @@ class CqlDurationGene( months.randomize(randomness, tryToForceNewValue) days.randomize(randomness, tryToForceNewValue) nanos.randomize(randomness, tryToForceNewValue) - negative.randomize(randomness, tryToForceNewValue) + isNegative.randomize(randomness, tryToForceNewValue) } override fun getValueAsPrintableString( @@ -70,7 +70,7 @@ class CqlDurationGene( * all zero. */ override fun getValueAsRawString(): String { - val sign = if (negative.value) "-" else "" + val sign = if (isNegative.value) "-" else "" return "$sign${months.value}mo${days.value}d${nanos.value}ns" } @@ -82,7 +82,7 @@ class CqlDurationGene( return this.months.unsafeCopyValueFrom(other.months) && this.days.unsafeCopyValueFrom(other.days) && this.nanos.unsafeCopyValueFrom(other.nanos) - && this.negative.unsafeCopyValueFrom(other.negative) + && this.isNegative.unsafeCopyValueFrom(other.isNegative) } override fun containsSameValueAs(other: Gene): Boolean { @@ -93,7 +93,7 @@ class CqlDurationGene( return this.months.containsSameValueAs(other.months) && this.days.containsSameValueAs(other.days) && this.nanos.containsSameValueAs(other.nanos) - && this.negative.containsSameValueAs(other.negative) + && this.isNegative.containsSameValueAs(other.isNegative) } override fun customShouldApplyShallowMutation( diff --git a/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraInsertBuilderTest.kt b/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraInsertBuilderTest.kt index 6841e85809..2ca9b0dc46 100644 --- a/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraInsertBuilderTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraInsertBuilderTest.kt @@ -1,5 +1,7 @@ package org.evomaster.core.database.cassandra +import org.evomaster.client.java.controller.api.dto.database.cassandra.CassandraColumnDto +import org.evomaster.client.java.controller.api.dto.database.cassandra.CassandraTableSchemaDto import org.evomaster.core.search.gene.UUIDGene import org.evomaster.core.search.gene.string.StringGene import org.junit.jupiter.api.Assertions.assertEquals @@ -12,9 +14,19 @@ class CassandraInsertBuilderTest { private val builder = CassandraInsertBuilder() + private fun schema(keyspace: String, table: String, vararg columns: CassandraColumnDto) = + CassandraTableSchemaDto(keyspace, table, columns.toList()) + + private fun column(name: String, cqlType: String) = CassandraColumnDto(name, cqlType, false, false) + + private fun partitionKey(name: String, cqlType: String) = CassandraColumnDto(name, cqlType, true, false) + + private fun clusteringColumn(name: String, cqlType: String) = CassandraColumnDto(name, cqlType, false, true) + @Test fun testOneGenePerColumn() { - val action = builder.createCassandraInsertionAction("ks", "users", "id uuid PARTITION KEY, name text") + val action = builder.createCassandraInsertionAction( + schema("ks", "users", partitionKey("id", "uuid"), column("name", "text"))) assertEquals(listOf("id", "name"), action.seeTopGenes().map { it.name }) assertTrue(action.seeTopGenes()[0] is UUIDGene) @@ -23,7 +35,8 @@ class CassandraInsertBuilderTest { @Test fun testKeyspaceAndTableAreKept() { - val action = builder.createCassandraInsertionAction("ks", "users", "id uuid PARTITION KEY") + val action = builder.createCassandraInsertionAction( + schema("ks", "users", partitionKey("id", "uuid"))) assertEquals("ks", action.keyspace) assertEquals("users", action.table) @@ -31,7 +44,8 @@ class CassandraInsertBuilderTest { @Test fun testActionName() { - val action = builder.createCassandraInsertionAction("ks", "users", "id uuid PARTITION KEY") + val action = builder.createCassandraInsertionAction( + schema("ks", "users", partitionKey("id", "uuid"))) assertEquals("CASSANDRA_Insert_ks_users", action.getName()) } @@ -39,7 +53,10 @@ class CassandraInsertBuilderTest { @Test fun testKeyRolesAreKept() { val action = builder.createCassandraInsertionAction( - "ks", "events", "id uuid PARTITION KEY, created timestamp CLUSTERING, note text") + schema("ks", "events", + partitionKey("id", "uuid"), + clusteringColumn("created", "timestamp"), + column("note", "text"))) assertTrue(action.columns[0].isPartitionKey) assertTrue(action.columns[1].isClusteringColumn) @@ -53,7 +70,10 @@ class CassandraInsertBuilderTest { @Test fun testColumnsWithUnsupportedTypeAreSkipped() { val action = builder.createCassandraInsertionAction( - "ks", "users", "id uuid PARTITION KEY, picture blob, name text") + schema("ks", "users", + partitionKey("id", "uuid"), + column("picture", "blob"), + column("name", "text"))) assertEquals(listOf("id", "name"), action.seeTopGenes().map { it.name }) assertEquals(listOf("id", "name"), action.columns.map { it.name }) @@ -64,10 +84,10 @@ class CassandraInsertBuilderTest { */ @Test fun testTableWithNoSupportedColumnIsRejected() { - assertThrows { - builder.createCassandraInsertionAction("ks", "blobs", "content blob") - } - assertFalse(builder.canBuildInsertionFor("content blob")) + val schema = schema("ks", "blobs", column("content", "blob")) + + assertThrows { builder.createCassandraInsertionAction(schema) } + assertFalse(builder.canBuildInsertionFor(schema)) } /** @@ -76,31 +96,39 @@ class CassandraInsertBuilderTest { */ @Test fun testTableWithUnsupportedPartitionKeyIsRejected() { - assertThrows { - builder.createCassandraInsertionAction("ks", "users", "id blob PARTITION KEY, name text") - } - assertFalse(builder.canBuildInsertionFor("id blob PARTITION KEY, name text")) + val schema = schema("ks", "users", partitionKey("id", "blob"), column("name", "text")) + + assertThrows { builder.createCassandraInsertionAction(schema) } + assertFalse(builder.canBuildInsertionFor(schema)) } @Test fun testTableWithUnsupportedClusteringColumnIsRejected() { - val schema = "id uuid PARTITION KEY, at blob CLUSTERING, note text" + val schema = schema("ks", "events", + partitionKey("id", "uuid"), + clusteringColumn("at", "blob"), + column("note", "text")) - assertThrows { - builder.createCassandraInsertionAction("ks", "events", schema) - } + assertThrows { builder.createCassandraInsertionAction(schema) } assertFalse(builder.canBuildInsertionFor(schema)) } @Test fun testInsertionCanBeBuiltWhenOnlyRegularColumnsAreSkipped() { - assertTrue(builder.canBuildInsertionFor("id uuid PARTITION KEY, picture blob, name text")) - assertTrue(builder.canBuildInsertionFor("id uuid PARTITION KEY, name text")) + assertTrue(builder.canBuildInsertionFor( + schema("ks", "users", + partitionKey("id", "uuid"), + column("picture", "blob"), + column("name", "text")))) + + assertTrue(builder.canBuildInsertionFor( + schema("ks", "users", partitionKey("id", "uuid"), column("name", "text")))) } @Test fun testCopyKeepsTheColumns() { - val action = builder.createCassandraInsertionAction("ks", "users", "id uuid PARTITION KEY, name text") + val action = builder.createCassandraInsertionAction( + schema("ks", "users", partitionKey("id", "uuid"), column("name", "text"))) val copy = action.copy() as CassandraDbAction assertEquals(action.keyspace, copy.keyspace) @@ -108,4 +136,4 @@ class CassandraInsertBuilderTest { assertEquals(action.columns, copy.columns) assertEquals(action.seeTopGenes().map { it.name }, copy.seeTopGenes().map { it.name }) } -} +} \ No newline at end of file diff --git a/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraLiteralRendererTest.kt b/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraLiteralRendererTest.kt index eae3be0013..00a06452b6 100644 --- a/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraLiteralRendererTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraLiteralRendererTest.kt @@ -89,7 +89,7 @@ class CassandraLiteralRendererTest { assertEquals("1mo2d3ns", CassandraLiteralRenderer.toCqlLiteral(gene)) - gene.negative.value = true + gene.isNegative.value = true assertEquals("-1mo2d3ns", CassandraLiteralRenderer.toCqlLiteral(gene)) } diff --git a/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraTableSchemaParserTest.kt b/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraTableSchemaParserTest.kt deleted file mode 100644 index feed13518f..0000000000 --- a/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraTableSchemaParserTest.kt +++ /dev/null @@ -1,99 +0,0 @@ -package org.evomaster.core.database.cassandra - -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows - -class CassandraTableSchemaParserTest { - - @Test - fun testEmptySchema() { - assertTrue(CassandraTableSchemaParser.parse("").isEmpty()) - } - - @Test - fun testSingleRegularColumn() { - val columns = CassandraTableSchemaParser.parse("name text") - - assertEquals(1, columns.size) - assertEquals(CassandraColumn("name", "text"), columns[0]) - } - - @Test - fun testPartitionKeyColumn() { - val columns = CassandraTableSchemaParser.parse("id uuid PARTITION KEY") - - assertEquals(listOf(CassandraColumn("id", "uuid", isPartitionKey = true)), columns) - } - - @Test - fun testClusteringColumn() { - val columns = CassandraTableSchemaParser.parse("created timestamp CLUSTERING") - - assertEquals(listOf(CassandraColumn("created", "timestamp", isClusteringColumn = true)), columns) - } - - @Test - fun testColumnMarkedBothAsPartitionKeyAndClustering() { - val columns = CassandraTableSchemaParser.parse("id uuid PARTITION KEY CLUSTERING") - - assertEquals( - listOf(CassandraColumn("id", "uuid", isPartitionKey = true, isClusteringColumn = true)), - columns - ) - } - - @Test - fun testSeveralColumnsKeepTheirOrder() { - val columns = CassandraTableSchemaParser.parse("id uuid PARTITION KEY, name text, created timestamp CLUSTERING") - - assertEquals( - listOf( - CassandraColumn("id", "uuid", isPartitionKey = true), - CassandraColumn("name", "text"), - CassandraColumn("created", "timestamp", isClusteringColumn = true) - ), - columns - ) - } - - /** - * The type of a collection is itself rendered with the same separator used between columns. - */ - @Test - fun testCollectionTypeIsNotSplit() { - val columns = CassandraTableSchemaParser.parse("id uuid PARTITION KEY, data map") - - assertEquals(2, columns.size) - assertEquals(CassandraColumn("data", "map"), columns[1]) - } - - @Test - fun testNestedCollectionTypeIsNotSplit() { - val columns = CassandraTableSchemaParser.parse("data map>>, name text") - - assertEquals(2, columns.size) - assertEquals(CassandraColumn("data", "map>>"), columns[0]) - assertEquals(CassandraColumn("name", "text"), columns[1]) - } - - @Test - fun testColumnWithNoTypeIsRejected() { - assertThrows { CassandraTableSchemaParser.parse("name") } - } - - /** - * With unbalanced type parameters, there is no telling which of the separators are the ones - * between columns, so the description is rejected instead of being split at the wrong places. - */ - @Test - fun testUnclosedTypeParametersAreRejected() { - assertThrows { CassandraTableSchemaParser.parse("tags map { CassandraTableSchemaParser.parse("a text>, b int") } - } -} diff --git a/core/src/test/kotlin/org/evomaster/core/search/gene/cassandra/CqlDurationGeneTest.kt b/core/src/test/kotlin/org/evomaster/core/search/gene/cassandra/CqlDurationGeneTest.kt index 4c00d4fd66..324719a302 100644 --- a/core/src/test/kotlin/org/evomaster/core/search/gene/cassandra/CqlDurationGeneTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/search/gene/cassandra/CqlDurationGeneTest.kt @@ -15,7 +15,7 @@ class CqlDurationGeneTest { months = IntegerGene("months", months), days = IntegerGene("days", days), nanos = LongGene("nanos", nanos) - ).apply { this.negative.value = negative } + ).apply { this.isNegative.value = negative } @Test fun testValueIsRenderedWithTheThreeUnits() { @@ -41,7 +41,7 @@ class CqlDurationGeneTest { @Test fun testDurationIsPositiveByDefault() { - assertFalse(CqlDurationGene("elapsed").negative.value) + assertFalse(CqlDurationGene("elapsed").isNegative.value) } @Test From 50cef3d66e2cdb02d8d5d52febe2da4512bf6a58 Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Wed, 16 Sep 2026 15:36:27 -0300 Subject: [PATCH 8/8] Add non-null requirements --- .../cassandra/CassandraColumnDto.java | 17 +++++++++++++---- .../cassandra/CassandraTableSchemaDto.java | 19 +++++++++++++------ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/cassandra/CassandraColumnDto.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/cassandra/CassandraColumnDto.java index 665957290d..a73b098daf 100644 --- a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/cassandra/CassandraColumnDto.java +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/cassandra/CassandraColumnDto.java @@ -1,5 +1,7 @@ package org.evomaster.client.java.controller.api.dto.database.cassandra; +import java.util.Objects; + /** * A single column of a Cassandra table, as read from the driver's own metadata on the SUT side and * reported to the core process. @@ -29,9 +31,16 @@ public class CassandraColumnDto { public CassandraColumnDto() { } + /** + * @param name the name of the column + * @param cqlType the type of the column, as named in CQL + * @param partitionKey whether this column is part of the partition key of the table + * @param clusteringColumn whether this column is one of the clustering columns of the table + * @throws NullPointerException if the name or the type is null + */ public CassandraColumnDto(String name, String cqlType, boolean partitionKey, boolean clusteringColumn) { - this.name = name; - this.cqlType = cqlType; + this.name = Objects.requireNonNull(name, "name cannot be null"); + this.cqlType = Objects.requireNonNull(cqlType, "cqlType cannot be null"); this.partitionKey = partitionKey; this.clusteringColumn = clusteringColumn; } @@ -41,7 +50,7 @@ public String getName() { } public void setName(String name) { - this.name = name; + this.name = Objects.requireNonNull(name, "name cannot be null"); } public String getCqlType() { @@ -49,7 +58,7 @@ public String getCqlType() { } public void setCqlType(String cqlType) { - this.cqlType = cqlType; + this.cqlType = Objects.requireNonNull(cqlType, "cqlType cannot be null"); } public boolean isPartitionKey() { diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/cassandra/CassandraTableSchemaDto.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/cassandra/CassandraTableSchemaDto.java index 4a0160e783..1de853e48d 100644 --- a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/cassandra/CassandraTableSchemaDto.java +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/cassandra/CassandraTableSchemaDto.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * The shape of a Cassandra table, ie the columns a row of it is composed of, as read from the @@ -25,10 +26,16 @@ public class CassandraTableSchemaDto { public CassandraTableSchemaDto() { } + /** + * @param keyspaceName the keyspace the table belongs to + * @param tableName the name of the table + * @param columns all the columns of the table + * @throws NullPointerException if any of the arguments is null + */ public CassandraTableSchemaDto(String keyspaceName, String tableName, List columns) { - this.keyspaceName = keyspaceName; - this.tableName = tableName; - this.columns = columns; + this.keyspaceName = Objects.requireNonNull(keyspaceName, "keyspaceName cannot be null"); + this.tableName = Objects.requireNonNull(tableName, "tableName cannot be null"); + this.columns = Objects.requireNonNull(columns, "columns cannot be null"); } public String getKeyspaceName() { @@ -36,7 +43,7 @@ public String getKeyspaceName() { } public void setKeyspaceName(String keyspaceName) { - this.keyspaceName = keyspaceName; + this.keyspaceName = Objects.requireNonNull(keyspaceName, "keyspaceName cannot be null"); } public String getTableName() { @@ -44,7 +51,7 @@ public String getTableName() { } public void setTableName(String tableName) { - this.tableName = tableName; + this.tableName = Objects.requireNonNull(tableName, "tableName cannot be null"); } public List getColumns() { @@ -52,6 +59,6 @@ public List getColumns() { } public void setColumns(List columns) { - this.columns = columns; + this.columns = Objects.requireNonNull(columns, "columns cannot be null"); } } \ No newline at end of file