From 536b39b52ebb521882fbc46614b3838af2a2a1cb Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Mon, 31 Aug 2026 17:54:26 -0300 Subject: [PATCH 1/3] Add Genes for cql types inet, list, set, and map --- .../insertions/CassandraScriptRunnerTest.java | 46 ++++++++- .../cassandra/CassandraColumnGeneBuilder.kt | 68 +++++++++++-- .../cassandra/CassandraLiteralRenderer.kt | 42 ++++++-- .../cassandra/CassandraTableSchemaParser.kt | 43 +-------- .../cassandra/CqlCollectionTypeParser.kt | 87 +++++++++++++++++ .../database/cassandra/CqlTypeParameters.kt | 57 +++++++++++ .../gene/cassandra/CqlCollectionGene.kt | 91 ++++++++++++++++++ .../gene/cassandra/CqlCollectionKind.kt | 41 ++++++++ .../CassandraColumnGeneBuilderTest.kt | 81 +++++++++++++++- .../cassandra/CassandraInsertBuilderTest.kt | 17 ++++ .../cassandra/CassandraLiteralRendererTest.kt | 96 +++++++++++++++++++ .../cassandra/CqlCollectionTypeParserTest.kt | 82 ++++++++++++++++ .../core/search/gene/GeneNumberOfGenesTest.kt | 2 +- .../core/search/gene/GeneSamplerForTests.kt | 12 +++ .../gene/cassandra/CqlCollectionGeneTest.kt | 93 ++++++++++++++++++ 15 files changed, 798 insertions(+), 60 deletions(-) create mode 100644 core/src/main/kotlin/org/evomaster/core/database/cassandra/CqlCollectionTypeParser.kt create mode 100644 core/src/main/kotlin/org/evomaster/core/database/cassandra/CqlTypeParameters.kt create mode 100644 core/src/main/kotlin/org/evomaster/core/search/gene/cassandra/CqlCollectionGene.kt create mode 100644 core/src/main/kotlin/org/evomaster/core/search/gene/cassandra/CqlCollectionKind.kt create mode 100644 core/src/test/kotlin/org/evomaster/core/database/cassandra/CqlCollectionTypeParserTest.kt create mode 100644 core/src/test/kotlin/org/evomaster/core/search/gene/cassandra/CqlCollectionGeneTest.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 8abfdd7561..715d88b69f 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,8 @@ 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, elapsed duration)"); + " (id int PRIMARY KEY, name text, elapsed duration, ip inet," + + " tags set, scores list, favs map)"); } @AfterAll @@ -97,6 +98,49 @@ public void testInsertDuration() { assertEquals(2, connection.execute("SELECT * FROM " + KEYSPACE + "." + TABLE).all().size()); } + /** + * An IP address is written as a quoted literal, whereas a collection is written as a delimited + * sequence of the literals of what it holds, with a list between square brackets and a set and + * a map between braces. This is what CassandraLiteralRenderer in the core module relies on when + * rendering a CqlCollectionGene and an InetGene, so it is checked here against a real Cassandra. + */ + @Test + public void testInsertInetAndCollections() { + List insertions = CassandraDsl.cassandra() + .insertInto(KEYSPACE, TABLE) + .d("id", "1") + .d("ip", "'127.0.0.1'") + .d("tags", "{'pet', 'cute'}") + .d("scores", "[17, 4, 2]") + .d("favs", "{'fruit': 3}") + .dtos(); + + CassandraInsertionResultsDto resultsDto = CassandraScriptRunner.executeInsert(connection, insertions); + + assertTrue(resultsDto.executionResults.get(0)); + assertEquals(1, connection.execute("SELECT * FROM " + KEYSPACE + "." + TABLE).all().size()); + } + + /** + * A collection gene can be randomized into an empty one, so the literal it renders has to be + * accepted as well. Note that Cassandra stores an empty collection as null. + */ + @Test + public void testInsertEmptyCollections() { + List insertions = CassandraDsl.cassandra() + .insertInto(KEYSPACE, TABLE) + .d("id", "1") + .d("tags", "{}") + .d("scores", "[]") + .d("favs", "{}") + .dtos(); + + CassandraInsertionResultsDto resultsDto = CassandraScriptRunner.executeInsert(connection, insertions); + + assertTrue(resultsDto.executionResults.get(0)); + assertEquals(1, connection.execute("SELECT * FROM " + KEYSPACE + "." + TABLE).all().size()); + } + @Test public void testInsertionFailureDoesNotStopFollowingInsertions() { 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..6307946c0a 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,25 +3,40 @@ 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.CqlCollectionGene +import org.evomaster.core.search.gene.cassandra.CqlCollectionKind import org.evomaster.core.search.gene.cassandra.CqlDurationGene +import org.evomaster.core.search.gene.collection.ArrayGene +import org.evomaster.core.search.gene.collection.FixedMapGene 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.network.InetGene 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. * + * The collection types are handled by recursing on the types parameterizing them, so a column is + * only supported when all of the types composing it are. + * * 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 + * - no gene generating a value of that type has been written yet, ie blob, the tuples, the vectors * and the user defined types. */ object CassandraColumnGeneBuilder { + /** + * The name given to the genes generating what a collection holds. Such genes are not bound to a + * column of their own, and the elements of a collection are written with no name in a CQL + * literal, so the name is only there to identify them while debugging. + */ + private const val ELEMENT_GENE_NAME = "element" + /** * 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 @@ -41,6 +56,11 @@ object CassandraColumnGeneBuilder { "double" to { name -> DoubleGene(name) }, "boolean" to { name -> BooleanGene(name) }, "uuid" to { name -> UUIDGene(name) }, + /* + Only IPv4 addresses are generated for now, although the CQL type also accepts IPv6 ones, as + that is what InetGene builds. The same restriction already applies to the SQL types. + */ + "inet" to { name -> InetGene(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. @@ -52,23 +72,55 @@ object CassandraColumnGeneBuilder { ) /** - * @return whether a gene can be built for [column], ie whether its CQL type is one of the - * scalar types handled here + * @return whether a gene can be built for [column], ie whether its CQL type is one of the ones + * handled here, or a collection of such types */ - fun isSupported(column: CassandraColumn) = normalize(column.cqlType) in GENE_BUILDERS + fun isSupported(column: CassandraColumn) = isSupported(normalize(column.cqlType)) /** * @throws IllegalArgumentException if the CQL type of [column] is not handled, as verifiable * beforehand with [isSupported] */ - fun buildGene(column: CassandraColumn): Gene { + fun buildGene(column: CassandraColumn): Gene = buildGene(column.name, normalize(column.cqlType)) + + private fun isSupported(cqlType: String): Boolean { - val builder = GENE_BUILDERS[normalize(column.cqlType)] - ?: throw IllegalArgumentException("Cannot handle the CQL type of column $column") + val collection = CqlCollectionTypeParser.parse(cqlType) ?: return cqlType in GENE_BUILDERS - return builder(column.name) + return collection.parameters.all { isSupported(normalize(it)) } } + /** + * @param cqlType a normalized CQL type + */ + private fun buildGene(name: String, cqlType: String): Gene { + + CqlCollectionTypeParser.parse(cqlType)?.let { return buildCollectionGene(name, it) } + + val builder = GENE_BUILDERS[cqlType] + ?: throw IllegalArgumentException("Cannot handle the CQL type $cqlType of column $name") + + return builder(name) + } + + private fun buildCollectionGene(name: String, type: CqlCollectionType): Gene { + + val content = when (type.kind) { + CqlCollectionKind.LIST -> ArrayGene(name, template = elementGene(type, 0)) + /* + Cassandra collapses the repeated elements of a set literal into a single one, so + generating them would just be wasted search effort. + */ + CqlCollectionKind.SET -> ArrayGene(name, template = elementGene(type, 0), uniqueElements = true) + CqlCollectionKind.MAP -> FixedMapGene(name, key = elementGene(type, 0), value = elementGene(type, 1)) + } + + return CqlCollectionGene(name, type.kind, content) + } + + private fun elementGene(type: CqlCollectionType, index: Int) = + buildGene(ELEMENT_GENE_NAME, normalize(type.parameters[index])) + private fun normalize(cqlType: String) = cqlType.trim().lowercase() } 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 264c42a2a1..c427ac97c8 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,10 +3,14 @@ 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.CqlCollectionGene import org.evomaster.core.search.gene.cassandra.CqlDurationGene +import org.evomaster.core.search.gene.collection.FixedMapGene +import org.evomaster.core.search.gene.collection.PairGene 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.network.InetGene import org.evomaster.core.search.gene.numeric.NumberGene import org.evomaster.core.search.gene.string.StringGene @@ -14,8 +18,9 @@ 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, uuids and durations are not. + * client side, and how a value has to be written depends on its type: text, the temporal types and + * the IP addresses are enclosed in single quotes, whereas numbers, booleans, uuids and durations are + * not, and the collections are written as a delimited sequence of the literals of what they hold. */ object CassandraLiteralRenderer { @@ -26,21 +31,44 @@ object CassandraLiteralRenderer { */ private const val ESCAPED_SINGLE_QUOTE = "''" + private const val ELEMENT_SEPARATOR = ", " + + private const val KEY_VALUE_SEPARATOR = ": " + /** * @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<*>, is CqlDurationGene -> value + is StringGene, is DateGene, is TimeGene, is DateTimeGene, is InetGene -> quote(gene.getValueAsRawString()) + is BooleanGene, is UUIDGene, is NumberGene<*>, is CqlDurationGene -> gene.getValueAsRawString() + is CqlCollectionGene -> renderCollection(gene) else -> throw IllegalArgumentException("Cannot render a CQL literal for a gene of type ${gene.javaClass.simpleName}") } } + /** + * The elements are rendered by recursing, rather than by asking the collection gene to print + * itself, as each of them has to be written the way a CQL literal of its own type is, eg with a + * text enclosed in single quotes rather than in the double quotes a gene prints itself with. + */ + private fun renderCollection(gene: CqlCollectionGene): String { + + val content = gene.content + + val entries = when (content) { + is FixedMapGene<*, *> -> content.getViewOfChildren().map { renderEntry(it as PairGene<*, *>) } + else -> content.getViewOfChildren().map { toCqlLiteral(it) } + } + + return entries.joinToString(ELEMENT_SEPARATOR, gene.kind.opening, gene.kind.closing) + } + + private fun renderEntry(entry: PairGene<*, *>) = + toCqlLiteral(entry.first) + KEY_VALUE_SEPARATOR + toCqlLiteral(entry.second) + private fun quote(value: String) = SINGLE_QUOTE + value.replace(SINGLE_QUOTE, ESCAPED_SINGLE_QUOTE) + SINGLE_QUOTE -} +} \ 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 index 6d917f2540..a1c684678c 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 @@ -16,10 +16,6 @@ object CassandraTableSchemaParser { 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 @@ -40,43 +36,8 @@ object CassandraTableSchemaParser { * @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 splitColumns(tableSchema: String) = + CqlTypeParameters.splitAtTopLevel(tableSchema, COLUMN_SEPARATOR) private fun parseColumn(description: String): CassandraColumn { diff --git a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CqlCollectionTypeParser.kt b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CqlCollectionTypeParser.kt new file mode 100644 index 0000000000..da8468dd29 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CqlCollectionTypeParser.kt @@ -0,0 +1,87 @@ +package org.evomaster.core.database.cassandra + +import org.evomaster.core.search.gene.cassandra.CqlCollectionKind + +/** + * Recognizes the CQL collection types and recovers their parameters, ie the CQL types of what they + * hold, so that a gene can be built for each of them. + */ +object CqlCollectionTypeParser { + + private const val FROZEN_PREFIX = "frozen" + + private const val TYPE_PARAMETER_SEPARATOR = ',' + + /** + * @param cqlType a normalized, ie trimmed and lower case, CQL type name + * @return the kind and parameters of [cqlType] if it is a collection type, null if it is not, + * which covers both the scalar types and the parameterized ones that are not collections, ie + * the tuples and the vectors + * @throws IllegalArgumentException if [cqlType] is a collection type carrying the wrong number + * of parameters, or if its type parameters are not balanced + */ + fun parse(cqlType: String): CqlCollectionType? { + + val unfrozen = stripFrozen(cqlType) + + if (!unfrozen.endsWith(CqlTypeParameters.END)) { + return null + } + + val parametersStart = unfrozen.indexOf(CqlTypeParameters.START) + if (parametersStart <= 0) { + return null + } + + val kind = CqlCollectionKind.entries.find { it.cqlName == unfrozen.substring(0, parametersStart).trim() } + ?: return null + + val parameters = CqlTypeParameters + .splitAtTopLevel(unfrozen.substring(parametersStart + 1, unfrozen.length - 1), TYPE_PARAMETER_SEPARATOR) + .map { it.trim() } + + if (parameters.size != kind.arity) { + throw IllegalArgumentException("A CQL ${kind.cqlName} is parameterized by ${kind.arity}" + + " type(s), but ${parameters.size} were given: $cqlType") + } + + return CqlCollectionType(kind, parameters) + } + + /** + * A collection is written as "frozen<...>" when its value is stored as a single immutable one, + * which is required of the collections nested inside another one and of the ones composing a + * primary key. The distinction does not affect how a value of it is written in an insertion, + * so the marker is just peeled off. + * + * Note that the SUT is not expected to report a frozen type in the first place, as the driver + * metadata the schema is read from is asked for the type without it. This is only here so that + * a type written by hand is handled the same way. + */ + private fun stripFrozen(cqlType: String): String { + + var current = cqlType + + while (current.startsWith(FROZEN_PREFIX + CqlTypeParameters.START) && current.endsWith(CqlTypeParameters.END)) { + current = current.substring(FROZEN_PREFIX.length + 1, current.length - 1).trim() + } + + return current + } +} + +/** + * A CQL collection type, ie its kind and the CQL types of what it holds: the type of the elements + * for a list and a set, and the types of the keys and of the values for a map. + */ +data class CqlCollectionType( + + val kind: CqlCollectionKind, + + /** + * The CQL types parameterizing the collection, in the order they are written in, ie the type of + * the elements for a list and a set, and the types of the keys and then of the values for a map. + * There are exactly as many as the arity of [kind]. + */ + val parameters: List +) \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CqlTypeParameters.kt b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CqlTypeParameters.kt new file mode 100644 index 0000000000..f949d37ae8 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CqlTypeParameters.kt @@ -0,0 +1,57 @@ +package org.evomaster.core.database.cassandra + +/** + * Splitting of a CQL fragment on a separator, ignoring the separators nested inside a type + * parameter list, as a collection type is itself written with them, eg "map". + */ +internal object CqlTypeParameters { + + const val START = '<' + + const val END = '>' + + /** + * @param text the fragment to split + * @param separator the character to split [text] on, when not nested inside a type parameter list + * @return the parts of [text], in the same order, not trimmed + * @throws IllegalArgumentException if the type parameter lists in [text] are not balanced, as + * then there is no telling which of the separators are the ones being split on + */ + fun splitAtTopLevel(text: String, separator: Char): List { + + val parts = mutableListOf() + val current = StringBuilder() + var depth = 0 + + for (c in text) { + when { + c == START -> { + depth++ + current.append(c) + } + c == END -> { + if (depth == 0) { + throw IllegalArgumentException(unbalancedMessage(text)) + } + depth-- + current.append(c) + } + c == separator && depth == 0 -> { + parts.add(current.toString()) + current.clear() + } + else -> current.append(c) + } + } + + if (depth != 0) { + throw IllegalArgumentException(unbalancedMessage(text)) + } + + parts.add(current.toString()) + + return parts + } + + private fun unbalancedMessage(text: String) = "Unbalanced type parameters in a CQL fragment: $text" +} \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/cassandra/CqlCollectionGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/cassandra/CqlCollectionGene.kt new file mode 100644 index 0000000000..a05c0a0826 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/cassandra/CqlCollectionGene.kt @@ -0,0 +1,91 @@ +package org.evomaster.core.search.gene.cassandra + +import org.evomaster.core.output.OutputFormat +import org.evomaster.core.search.gene.Gene +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 one of the Cassandra collection types, ie a list, a set or a map. + * + * The [kind] is kept explicitly because the CQL literals of a list and of a set are built from the + * same kind of gene but written with different delimiters, and so could not be told apart otherwise. + */ +class CqlCollectionGene( + name: String, + val kind: CqlCollectionKind, + /** + * The elements of the collection, held in the gene handling that kind of collection, ie an + * ArrayGene for a list and a set, and a FixedMapGene for a map. All of the behaviour of this + * gene during the search is delegated to it. + */ + val content: Gene +) : CompositeFixedGene(name, mutableListOf(content)) { + + init { + if (!kind.isValidContent(content)) { + throw IllegalArgumentException("The elements of a CQL ${kind.cqlName} cannot be held in" + + " a ${content.javaClass.simpleName}") + } + } + + override fun copyContent(): Gene = CqlCollectionGene(name, kind, content.copy()) + + override fun checkForLocallyValidIgnoringChildren(): Boolean { + return true + } + + override fun randomize(randomness: Randomness, tryToForceNewValue: Boolean) { + content.randomize(randomness, tryToForceNewValue) + } + + /** + * Note that this is not how the collection is written in a CQL statement, as each of its + * elements would have to be written the way a CQL literal of its own type is. Building that + * literal is the job of CassandraLiteralRenderer, which recurses over [content]. + */ + override fun getValueAsPrintableString( + previousGenes: List, + mode: GeneUtils.EscapeMode?, + targetFormat: OutputFormat?, + extraCheck: Boolean + ): String { + return content.getValueAsPrintableString(previousGenes, mode, targetFormat, extraCheck) + } + + override fun getValueAsRawString(): String { + return content.getValueAsRawString() + } + + override fun isPrintable(): Boolean { + return content.isPrintable() + } + + override fun unsafeCopyValueFrom(other: Gene): Boolean { + if (other !is CqlCollectionGene || other.kind != this.kind) { + return false + } + + return this.content.unsafeCopyValueFrom(other.content) + } + + override fun containsSameValueAs(other: Gene): Boolean { + if (other !is CqlCollectionGene || other.kind != this.kind) { + return false + } + + return this.content.containsSameValueAs(other.content) + } + + 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/main/kotlin/org/evomaster/core/search/gene/cassandra/CqlCollectionKind.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/cassandra/CqlCollectionKind.kt new file mode 100644 index 0000000000..3bd82a1673 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/cassandra/CqlCollectionKind.kt @@ -0,0 +1,41 @@ +package org.evomaster.core.search.gene.cassandra + +import org.evomaster.core.search.gene.Gene +import org.evomaster.core.search.gene.collection.ArrayGene +import org.evomaster.core.search.gene.collection.FixedMapGene + +/** + * A Cassandra collection type, with how many CQL types parameterize it and how its literal is + * delimited, eg a list is written "[1, 2]" whereas a set is written "{1, 2}". + */ +enum class CqlCollectionKind( + + /** + * The name of the type in CQL, ie how it is written in a schema. + */ + val cqlName: String, + + /** + * How many CQL types parameterize this one, ie one for the elements of a list and of a set, two + * for the keys and the values of a map. + */ + val arity: Int, + + val opening: String, + + val closing: String +) { + + LIST("list", 1, "[", "]"), + + SET("set", 1, "{", "}"), + + MAP("map", 2, "{", "}"); + + /** + * @return whether [content] is the kind of gene holding the elements of a collection of this + * type, ie a [FixedMapGene] for a map and an [ArrayGene] for a list and a set + */ + fun isValidContent(content: Gene) = + if (this == MAP) content is FixedMapGene<*, *> else content is ArrayGene<*> +} \ No newline at end of file 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 30edf5a0a2..93d85365c3 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,7 +3,12 @@ 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.CqlCollectionGene +import org.evomaster.core.search.gene.cassandra.CqlCollectionKind import org.evomaster.core.search.gene.cassandra.CqlDurationGene +import org.evomaster.core.search.gene.collection.ArrayGene +import org.evomaster.core.search.gene.collection.FixedMapGene +import org.evomaster.core.search.gene.network.InetGene import org.evomaster.core.search.gene.datetime.DateGene import org.evomaster.core.search.gene.datetime.DateTimeGene import org.evomaster.core.search.gene.datetime.TimeGene @@ -88,14 +93,86 @@ class CassandraColumnGeneBuilderTest { assertTrue(buildFor("duration") is CqlDurationGene) } + @Test + fun testInetType() { + assertTrue(buildFor("inet") is InetGene) + } + + @Test + fun testListType() { + val gene = buildFor("list") as CqlCollectionGene + + assertEquals(CqlCollectionKind.LIST, gene.kind) + val content = gene.content as ArrayGene<*> + assertFalse(content.uniqueElements) + assertTrue(content.template is IntegerGene) + } + + /** + * Cassandra collapses the repeated elements of a set, so generating them is wasted effort. + */ + @Test + fun testSetTypeGeneratesUniqueElements() { + val gene = buildFor("set") as CqlCollectionGene + + assertEquals(CqlCollectionKind.SET, gene.kind) + val content = gene.content as ArrayGene<*> + assertTrue(content.uniqueElements) + assertTrue(content.template is StringGene) + } + + @Test + fun testMapType() { + val gene = buildFor("map") as CqlCollectionGene + + assertEquals(CqlCollectionKind.MAP, gene.kind) + val content = gene.content as FixedMapGene<*, *> + assertTrue(content.template.first is StringGene) + assertTrue(content.template.second is IntegerGene) + } + + @Test + fun testNestedCollectionType() { + val gene = buildFor("list>") as CqlCollectionGene + + assertEquals(CqlCollectionKind.LIST, gene.kind) + val element = (gene.content as ArrayGene<*>).template as CqlCollectionGene + assertEquals(CqlCollectionKind.SET, element.kind) + assertTrue((element.content as ArrayGene<*>).template is IntegerGene) + } + + /** + * Whether a collection is frozen does not change how a value of it is written in an insertion. + */ + @Test + fun testFrozenCollectionIsHandledAsAPlainOne() { + val gene = buildFor("frozen>") as CqlCollectionGene + + assertEquals(CqlCollectionKind.LIST, gene.kind) + assertTrue((gene.content as ArrayGene<*>).template is IntegerGene) + } + + /** + * No value can be generated for a collection when none can be generated for what it holds. + */ + @Test + fun testCollectionOfAnUnsupportedTypeIsNotSupported() { + listOf("list", "map", "set>", "list>").forEach { + assertFalse(CassandraColumnGeneBuilder.isSupported(CassandraColumn("aColumn", it)), "$it should not be supported") + assertThrows("no exception for $it") { buildFor(it) } + } + } + /** * 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. For the * other types, it is just that no gene generating a value for them has been written yet. + * The tuples and the vectors are written with type parameters without being collections, so + * they are the ones the handling of the collection types has to avoid mistaking for one. */ @Test fun testUnsupportedTypes() { - listOf("counter", "timeuuid", "blob", "inet", "list", "frozen").forEach { + listOf("counter", "timeuuid", "blob", "frozen", "tuple", "vector").forEach { assertFalse(CassandraColumnGeneBuilder.isSupported(CassandraColumn("aColumn", it)), "$it should not be supported") assertThrows("no exception for $it") { buildFor(it) } } @@ -103,7 +180,7 @@ class CassandraColumnGeneBuilderTest { @Test fun testSupportedTypesAreReportedAsSuch() { - listOf("text", "int", "uuid", "timestamp", "boolean").forEach { + listOf("text", "int", "uuid", "timestamp", "boolean", "inet", "list", "map").forEach { assertTrue(CassandraColumnGeneBuilder.isSupported(CassandraColumn("aColumn", it)), "$it should be supported") } } 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..b00dc2518c 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,6 +1,7 @@ package org.evomaster.core.database.cassandra import org.evomaster.core.search.gene.UUIDGene +import org.evomaster.core.search.gene.cassandra.CqlCollectionGene import org.evomaster.core.search.gene.string.StringGene import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse @@ -98,6 +99,22 @@ class CassandraInsertBuilderTest { assertTrue(builder.canBuildInsertionFor("id uuid PARTITION KEY, name text")) } + /** + * Cassandra only allows a frozen collection in a primary key, and a frozen type is reported as + * a plain one, so a collection in that position is handled as any other supported column. + */ + @Test + fun testTableWithACollectionAsPartitionKeyIsAccepted() { + val schema = "tags set PARTITION KEY, v int" + + assertTrue(builder.canBuildInsertionFor(schema)) + + val action = builder.createCassandraInsertionAction("ks", "images", schema) + + assertEquals(listOf("tags", "v"), action.seeTopGenes().map { it.name }) + assertTrue(action.seeTopGenes()[0] is CqlCollectionGene) + } + @Test fun testCopyKeepsTheColumns() { val action = builder.createCassandraInsertionAction("ks", "users", "id uuid PARTITION KEY, name text") 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..c06ed28460 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,7 +3,13 @@ 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.CqlCollectionGene +import org.evomaster.core.search.gene.cassandra.CqlCollectionKind import org.evomaster.core.search.gene.cassandra.CqlDurationGene +import org.evomaster.core.search.gene.collection.ArrayGene +import org.evomaster.core.search.gene.collection.FixedMapGene +import org.evomaster.core.search.gene.collection.PairGene +import org.evomaster.core.search.gene.network.InetGene import org.evomaster.core.search.gene.datetime.DateGene import org.evomaster.core.search.gene.datetime.DateTimeGene import org.evomaster.core.search.gene.datetime.TimeGene @@ -93,6 +99,96 @@ class CassandraLiteralRendererTest { assertEquals("-1mo2d3ns", CassandraLiteralRenderer.toCqlLiteral(gene)) } + /** + * An IP address is written as a quoted literal in CQL, ie an unquoted one is a syntax error. + */ + @Test + fun testInetIsQuoted() { + val gene = InetGene("ip") + + assertEquals("'${gene.getValueAsRawString()}'", CassandraLiteralRenderer.toCqlLiteral(gene)) + } + + private fun arrayGeneOf(vararg values: Int): ArrayGene { + + val gene = ArrayGene("elements", template = IntegerGene("element")) + values.forEach { gene.addElement(IntegerGene("element", it)) } + + return gene + } + + @Test + fun testListIsWrittenBetweenSquareBrackets() { + val gene = CqlCollectionGene("scores", CqlCollectionKind.LIST, arrayGeneOf(1, 2)) + + assertEquals("[1, 2]", CassandraLiteralRenderer.toCqlLiteral(gene)) + } + + @Test + fun testSetIsWrittenBetweenBraces() { + val gene = CqlCollectionGene("tags", CqlCollectionKind.SET, arrayGeneOf(1, 2)) + + assertEquals("{1, 2}", CassandraLiteralRenderer.toCqlLiteral(gene)) + } + + @Test + fun testMapIsWrittenAsKeysAndValues() { + val content = FixedMapGene("entries", key = StringGene("element"), value = IntegerGene("element")) + content.addElement(PairGene("entry", StringGene("element", "a"), IntegerGene("element", 1))) + + val gene = CqlCollectionGene("favs", CqlCollectionKind.MAP, content) + + assertEquals("{'a': 1}", CassandraLiteralRenderer.toCqlLiteral(gene)) + } + + /** + * The elements have to be written the way a CQL literal of their own type is, which is what + * would be lost by asking the collection gene to print itself instead of recursing. + */ + @Test + fun testTextElementsAreQuotedAndEscaped() { + val content = ArrayGene("elements", template = StringGene("element")) + content.addElement(StringGene("element", "a")) + content.addElement(StringGene("element", "l'Alice")) + + val gene = CqlCollectionGene("tags", CqlCollectionKind.SET, content) + + assertEquals("{'a', 'l''Alice'}", CassandraLiteralRenderer.toCqlLiteral(gene)) + } + + @Test + fun testEmptyCollectionsAreWrittenWithTheirDelimitersOnly() { + assertEquals("[]", CassandraLiteralRenderer.toCqlLiteral( + CqlCollectionGene("scores", CqlCollectionKind.LIST, arrayGeneOf()))) + + assertEquals("{}", CassandraLiteralRenderer.toCqlLiteral( + CqlCollectionGene("tags", CqlCollectionKind.SET, arrayGeneOf()))) + + assertEquals("{}", CassandraLiteralRenderer.toCqlLiteral(CqlCollectionGene( + "favs", + CqlCollectionKind.MAP, + FixedMapGene("entries", key = StringGene("element"), value = IntegerGene("element")) + ))) + } + + @Test + fun testNestedCollectionIsRenderedByRecursing() { + val content = FixedMapGene( + "entries", + key = StringGene("element"), + value = CqlCollectionGene("element", CqlCollectionKind.LIST, arrayGeneOf()) + ) + content.addElement(PairGene( + "entry", + StringGene("element", "a"), + CqlCollectionGene("element", CqlCollectionKind.LIST, arrayGeneOf(1, 2)) + )) + + val gene = CqlCollectionGene("data", CqlCollectionKind.MAP, content) + + assertEquals("{'a': [1, 2]}", CassandraLiteralRenderer.toCqlLiteral(gene)) + } + @Test fun testGeneWithNoCqlRepresentationIsRejected() { assertThrows { diff --git a/core/src/test/kotlin/org/evomaster/core/database/cassandra/CqlCollectionTypeParserTest.kt b/core/src/test/kotlin/org/evomaster/core/database/cassandra/CqlCollectionTypeParserTest.kt new file mode 100644 index 0000000000..016a6af209 --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/database/cassandra/CqlCollectionTypeParserTest.kt @@ -0,0 +1,82 @@ +package org.evomaster.core.database.cassandra + +import org.evomaster.core.search.gene.cassandra.CqlCollectionKind +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class CqlCollectionTypeParserTest { + + @Test + fun testList() { + assertEquals(CqlCollectionType(CqlCollectionKind.LIST, listOf("int")), CqlCollectionTypeParser.parse("list")) + } + + @Test + fun testSet() { + assertEquals(CqlCollectionType(CqlCollectionKind.SET, listOf("text")), CqlCollectionTypeParser.parse("set")) + } + + @Test + fun testMap() { + assertEquals( + CqlCollectionType(CqlCollectionKind.MAP, listOf("text", "int")), + CqlCollectionTypeParser.parse("map") + ) + } + + /** + * The comma separating the parameters of the nested collection is not one of the separators + * between the parameters of the map. + */ + @Test + fun testNestedCollectionIsNotSplitOn() { + assertEquals( + CqlCollectionType(CqlCollectionKind.MAP, listOf("text", "map")), + CqlCollectionTypeParser.parse("map>") + ) + } + + @Test + fun testFrozenMarkerIsPeeledOff() { + assertEquals(CqlCollectionType(CqlCollectionKind.LIST, listOf("int")), CqlCollectionTypeParser.parse("frozen>")) + } + + @Test + fun testFrozenMarkerOfANestedCollectionIsKeptForTheRecursion() { + assertEquals( + CqlCollectionType(CqlCollectionKind.MAP, listOf("text", "frozen>")), + CqlCollectionTypeParser.parse("map>>") + ) + } + + @Test + fun testScalarTypeIsNotACollection() { + listOf("int", "text", "duration", "inet").forEach { + assertNull(CqlCollectionTypeParser.parse(it), "$it should not be a collection") + } + } + + /** + * A tuple, a vector and a frozen user defined type are all written with type parameters without + * being collections, so they have to be told apart from the ones that are. + */ + @Test + fun testOtherParameterizedTypesAreNotCollections() { + listOf("tuple", "vector", "frozen").forEach { + assertNull(CqlCollectionTypeParser.parse(it), "$it should not be a collection") + } + } + + @Test + fun testWrongNumberOfParametersIsRejected() { + assertThrows { CqlCollectionTypeParser.parse("map") } + assertThrows { CqlCollectionTypeParser.parse("list") } + } + + @Test + fun testUnbalancedTypeParametersAreRejected() { + assertThrows { CqlCollectionTypeParser.parse("map") } + } +} \ 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 a090467d41..32654cdd1c 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(96, geneClasses.size) + assertEquals(97, 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 4528d42cd5..c5a289f0c6 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,8 @@ 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.CqlCollectionGene +import org.evomaster.core.search.gene.cassandra.CqlCollectionKind import org.evomaster.core.search.gene.cassandra.CqlDurationGene import org.evomaster.core.search.gene.collection.* import org.evomaster.core.search.gene.datetime.* @@ -189,6 +191,7 @@ object GeneSamplerForTests { // Cassandra genes CqlDurationGene::class -> sampleCqlDurationGene(rand) as T + CqlCollectionGene::class -> sampleCqlCollectionGene(rand) as T // JSON Patch genes JsonPatchDocumentGene::class -> sampleJsonPatchDocumentGene(rand) as T @@ -427,6 +430,15 @@ object GeneSamplerForTests { return CqlDurationGene("rand CqlDurationGene ${rand.nextInt()}") } + private fun sampleCqlCollectionGene(rand: Randomness): CqlCollectionGene { + + val kind = rand.choose(CqlCollectionKind.entries) + + val content = if (kind == CqlCollectionKind.MAP) sampleFixedMapGene(rand) else sampleArrayGene(rand) + + return CqlCollectionGene("rand CqlCollectionGene ${rand.nextInt()}", kind, content) + } + 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/CqlCollectionGeneTest.kt b/core/src/test/kotlin/org/evomaster/core/search/gene/cassandra/CqlCollectionGeneTest.kt new file mode 100644 index 0000000000..1f753d3202 --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/search/gene/cassandra/CqlCollectionGeneTest.kt @@ -0,0 +1,93 @@ +package org.evomaster.core.search.gene.cassandra + +import org.evomaster.core.search.gene.collection.ArrayGene +import org.evomaster.core.search.gene.collection.FixedMapGene +import org.evomaster.core.search.gene.numeric.IntegerGene +import org.evomaster.core.search.gene.sql.SqlAutoIncrementGene +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 CqlCollectionGeneTest { + + private fun arrayOfInts(vararg values: Int): ArrayGene { + + val gene = ArrayGene("elements", template = IntegerGene("element")) + values.forEach { gene.addElement(IntegerGene("element", it)) } + + return gene + } + + private fun mapOfTextToInt() = + FixedMapGene("entries", key = StringGene("element"), value = IntegerGene("element")) + + @Test + fun testMapCannotBeBuiltFromAnArray() { + assertThrows { + CqlCollectionGene("favs", CqlCollectionKind.MAP, arrayOfInts()) + } + } + + @Test + fun testListAndSetCannotBeBuiltFromAMap() { + assertThrows { + CqlCollectionGene("scores", CqlCollectionKind.LIST, mapOfTextToInt()) + } + assertThrows { + CqlCollectionGene("tags", CqlCollectionKind.SET, mapOfTextToInt()) + } + } + + @Test + fun testCopyKeepsTheKindAndTheContent() { + val gene = CqlCollectionGene("scores", CqlCollectionKind.LIST, arrayOfInts(1, 2)) + + val copy = gene.copy() as CqlCollectionGene + + assertEquals(CqlCollectionKind.LIST, copy.kind) + assertEquals(2, (copy.content as ArrayGene<*>).getViewOfChildren().size) + assertTrue(gene.containsSameValueAs(copy)) + } + + /** + * A list and a set holding the same elements are not the same value, as they are not even + * written the same way in CQL. + */ + @Test + fun testListDoesNotContainTheSameValueAsASet() { + val list = CqlCollectionGene("elements", CqlCollectionKind.LIST, arrayOfInts(1, 2)) + val set = CqlCollectionGene("elements", CqlCollectionKind.SET, arrayOfInts(1, 2)) + + assertFalse(list.containsSameValueAs(set)) + assertFalse(set.containsSameValueAs(list)) + } + + @Test + fun testCollectionsWithDifferentElementsDoNotContainTheSameValue() { + val one = CqlCollectionGene("scores", CqlCollectionKind.LIST, arrayOfInts(1, 2)) + val other = CqlCollectionGene("scores", CqlCollectionKind.LIST, arrayOfInts(1, 3)) + + assertFalse(one.containsSameValueAs(other)) + } + + /** + * A gene that is not printable is left out of the insertion altogether, so a collection must + * not claim to be printable when what it holds is not. + */ + @Test + fun testIsPrintableFollowsTheContent() { + val printable = CqlCollectionGene("scores", CqlCollectionKind.LIST, arrayOfInts(1, 2)) + assertTrue(printable.isPrintable()) + + //any gene that is not printable would do here + val content = ArrayGene("elements", template = SqlAutoIncrementGene("element")) + content.addElement(SqlAutoIncrementGene("element")) + val notPrintable = CqlCollectionGene("scores", CqlCollectionKind.LIST, content) + + assertFalse(content.isPrintable()) + assertFalse(notPrintable.isPrintable()) + } +} \ No newline at end of file From b5f2ecd335b454fe300bfba84ac594f41f6889c8 Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Mon, 31 Aug 2026 18:16:36 -0300 Subject: [PATCH 2/3] Fix test name --- .../database/cassandra/CassandraColumnGeneBuilderTest.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 93d85365c3..d60d1e088c 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 @@ -110,9 +110,12 @@ class CassandraColumnGeneBuilderTest { /** * Cassandra collapses the repeated elements of a set, so generating them is wasted effort. + * Note that the gene only asks for unique elements, without guaranteeing them: the check is + * skipped altogether for the element types [ArrayGene] cannot compare, and nothing keeps an + * element from being mutated into the value of another one afterwards. */ @Test - fun testSetTypeGeneratesUniqueElements() { + fun testSetTypeAsksForUniqueElements() { val gene = buildFor("set") as CqlCollectionGene assertEquals(CqlCollectionKind.SET, gene.kind) From 324e59fc72cbcb4d483f2ba07cbb5866dbf60bdf Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Mon, 31 Aug 2026 20:03:27 -0300 Subject: [PATCH 3/3] Make range boundaries inclusive for BigDecimalGene --- .../org/evomaster/core/search/gene/numeric/BigDecimalGene.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/numeric/BigDecimalGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/numeric/BigDecimalGene.kt index 22ab592576..6ea07234e3 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/numeric/BigDecimalGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/numeric/BigDecimalGene.kt @@ -285,7 +285,7 @@ class BigDecimalGene( private fun setValueWithDecimal(bd: BigDecimal, precision: Int?, scale: Int?){ - val ensureRoundedValueIsInRange = (getMinimum () < bd && bd < getMaximum()) + val ensureRoundedValueIsInRange = (getMinimum () <= bd && bd <= getMaximum()) val rounded = if (precision == null){ if (scale == null) bd