Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
76bb1fb
Merge branch 'master' into feature/cql-heuristics-calculator
gonzalotguerrero Jun 19, 2026
92aa301
First calculator draft
gonzalotguerrero Jun 24, 2026
56c806a
Merge branch 'master' into feature/cql-heuristics-calculator
gonzalotguerrero Jun 24, 2026
c80e69d
Merge branch 'feature/cql-query-parser' into feature/cql-heuristics-c…
gonzalotguerrero Jun 30, 2026
6b6ad91
Fix comparison of time-related values
gonzalotguerrero Jun 30, 2026
059e9a5
Improve test class coverage
gonzalotguerrero Jun 30, 2026
8455eb6
Add support for integer constants in time-related types
gonzalotguerrero Jun 30, 2026
b6e870b
Merge branch 'master' into feature/cql-heuristics-calculator
gonzalotguerrero Jul 1, 2026
a796494
Created CassandraOperationEvaluator
gonzalotguerrero Jul 1, 2026
bcf43e2
Minor refactorings
gonzalotguerrero Jul 1, 2026
6dd2267
Add more replacements for overloaded execute method
gonzalotguerrero Jul 6, 2026
ec7e7f3
Add separate fields for keyspace and table in ExecutedCqlCommand
gonzalotguerrero Jul 6, 2026
d28282c
Merge branch 'master' into feature/cql-heuristics-calculator
gonzalotguerrero Jul 6, 2026
f8ccc59
Merge branch 'master' into feature/cql-session-replacement-v2
gonzalotguerrero Jul 6, 2026
f61c578
Merge branch 'feature/cql-heuristics-calculator' into feature/cql-ses…
gonzalotguerrero Jul 6, 2026
288be95
PR review comments
gonzalotguerrero Jul 8, 2026
8a566ee
Improve CqlParserUtils
gonzalotguerrero Jul 8, 2026
c9028dd
Merge branch 'master' into feature/cql-heuristics-calculator
gonzalotguerrero Jul 8, 2026
d19881f
Divide regex to improve readability
gonzalotguerrero Jul 9, 2026
5263ce5
Merge branch 'feature/cql-heuristics-calculator' into feature/cql-ses…
gonzalotguerrero Jul 9, 2026
432b4c7
Merge pull request #1629 from WebFuzzing/feature/cql-session-replacem…
arcuri82 Jul 10, 2026
2cc704e
Evaluator and parsers refactorings
gonzalotguerrero Jul 13, 2026
d2179ac
Improve parsing and comparison of Duration values:
gonzalotguerrero Jul 14, 2026
bfbe32b
More refactorings in evaluator
gonzalotguerrero Jul 14, 2026
37aa780
Refactorings and fix handling of unsupported operations
gonzalotguerrero Jul 20, 2026
e8af58a
Fix handling of missing columns in rows
gonzalotguerrero Jul 21, 2026
f30c556
Fix tests
gonzalotguerrero Jul 21, 2026
7671a3e
Add comment regarding NULL
gonzalotguerrero Jul 21, 2026
7ea66bc
Merge branch 'master' into feature/cql-heuristics-calculator
gonzalotguerrero Jul 21, 2026
ac4b916
Review comments:
gonzalotguerrero Jul 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.evomaster.client.java.utils;

/**
* The raw, un-combined components of a parsed ISO-8601 duration string, as produced by
* {@link IsoDurationParser}. Fields that didn't participate in the match default to {@code 0}.
*/
public class IsoDuration {

public final long years;
public final long months;
public final long weeks;
public final long days;
public final long hours;
public final long minutes;
public final long seconds;

public IsoDuration(long years, long months, long weeks, long days, long hours, long minutes, long seconds) {
this.years = years;
this.months = months;
this.weeks = weeks;
this.days = days;
this.hours = hours;
this.minutes = minutes;
this.seconds = seconds;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package org.evomaster.client.java.utils;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Parses ISO-8601 duration string literals (e.g. {@code P1Y2M3DT4H5M6S}) into an
* {@link IsoDuration} holding their raw, un-combined components.
*
* <p>Supports three formats:
*
* <ul>
* <li>General: {@code P1Y2M3DT4H5M6S}
* <li>Weeks only: {@code P3W}
* <li>Alternative: {@code P0001-02-03T04:05:06}
* </ul>
*
* <p>Input must be prefixed with {@code P} or {@code p}; no sign handling is done here (callers
* are expected to strip/apply any leading {@code -} themselves).
*/
public class IsoDurationParser {

private IsoDurationParser() {}

private static final char TIME_SEPARATOR = 'T';
private static final char YEAR_DESIGNATOR = 'Y';
private static final char MONTH_OR_MINUTE_DESIGNATOR = 'M';
private static final char DAY_DESIGNATOR = 'D';
private static final char HOUR_DESIGNATOR = 'H';
private static final char SECOND_DESIGNATOR = 'S';

private static final String DURATION_PREFIX_UPPER = "P";
private static final String DURATION_PREFIX_LOWER = "p";
private static final String WEEK_SUFFIX = "W";
private static final String ALTERNATIVE_DATE_SEPARATOR = "-";

/**
* Matches the general format, e.g. {@code P1Y2M3DT4H5M6S}. Every component is optional,
* so bare {@code P} or {@code PT} are valid (zero-length) durations.
*/
private static final Pattern GENERAL_PATTERN = Pattern.compile(
DURATION_PREFIX_UPPER
+ "(?:(\\d+)" + YEAR_DESIGNATOR + ")?"
+ "(?:(\\d+)" + MONTH_OR_MINUTE_DESIGNATOR + ")?"
+ "(?:(\\d+)" + DAY_DESIGNATOR + ")?"
+ "(?:" + TIME_SEPARATOR
+ "(?:(\\d+)" + HOUR_DESIGNATOR + ")?"
+ "(?:(\\d+)" + MONTH_OR_MINUTE_DESIGNATOR + ")?"
+ "(?:(\\d+)" + SECOND_DESIGNATOR + ")?)?");

/** Matches the weeks-only format, e.g. {@code P3W}. */
private static final Pattern WEEK_PATTERN =
Pattern.compile(DURATION_PREFIX_UPPER + "(\\d+)" + WEEK_SUFFIX);

/** Matches the alternative format, e.g. {@code P0001-02-03T04:05:06}. */
private static final Pattern ALTERNATIVE_PATTERN = Pattern.compile(
DURATION_PREFIX_UPPER + "(\\d{4})-(\\d{2})-(\\d{2})" + TIME_SEPARATOR + "(\\d{2}):(\\d{2}):(\\d{2})");

/**
* Parses an ISO-8601 duration literal, routing to whichever of the three supported formats
* applies: {@code P}-prefixed input ending in {@code W} is the weeks-only format;
* {@code P}-prefixed input containing {@code -} is the alternative format; anything else is
* the general format.
*
* @param isoDuration the duration literal to parse; must be prefixed with {@code P} or {@code p}
* @return the parsed duration, decomposed into its raw components
* @throws IllegalArgumentException if {@code isoDuration} isn't {@code P}/{@code p}-prefixed,
* or doesn't match the format implied by its suffix/content
*/
public static IsoDuration parse(String isoDuration) {
if (!isoDuration.startsWith(DURATION_PREFIX_UPPER) && !isoDuration.startsWith(DURATION_PREFIX_LOWER)) {
throw new IllegalArgumentException("Not an ISO-8601 duration literal: '" + isoDuration + "'");
}

String upper = isoDuration.toUpperCase();
if (upper.endsWith(WEEK_SUFFIX)) {
return parseWeekOnly(upper);
} else if (upper.contains(ALTERNATIVE_DATE_SEPARATOR)) {
return parseAlternative(upper);
} else {
return parseGeneral(upper);
}
}

private static IsoDuration parseGeneral(String isoDuration) {
Matcher matcher = GENERAL_PATTERN.matcher(isoDuration);
if (!matcher.matches()) {
throw new IllegalArgumentException("Unable to parse duration literal '" + isoDuration + "'");
} else {
long years = groupAsLong(matcher, 1);
long months = groupAsLong(matcher, 2);
long days = groupAsLong(matcher, 3);
long hours = groupAsLong(matcher, 4);
long minutes = groupAsLong(matcher, 5);
long seconds = groupAsLong(matcher, 6);

return new IsoDuration(years, months, 0, days, hours, minutes, seconds);
}
}

private static IsoDuration parseWeekOnly(String isoDuration) {
Matcher matcher = WEEK_PATTERN.matcher(isoDuration);
if (!matcher.matches()) {
throw new IllegalArgumentException("Unable to parse duration literal '" + isoDuration + "'");
} else {
long weeks = Long.parseLong(matcher.group(1));
return new IsoDuration(0, 0, weeks, 0, 0, 0, 0);
}
}

private static IsoDuration parseAlternative(String isoDuration) {
Matcher matcher = ALTERNATIVE_PATTERN.matcher(isoDuration);
if (!matcher.matches()) {
throw new IllegalArgumentException("Unable to parse duration literal '" + isoDuration + "'");
} else {
long years = Long.parseLong(matcher.group(1));
long months = Long.parseLong(matcher.group(2));
long days = Long.parseLong(matcher.group(3));
long hours = Long.parseLong(matcher.group(4));
long minutes = Long.parseLong(matcher.group(5));
long seconds = Long.parseLong(matcher.group(6));

return new IsoDuration(years, months, 0, days, hours, minutes, seconds);
}
}

/** Returns the matched group as a {@code long}, or {@code 0} if the (optional) group didn't participate. */
private static long groupAsLong(Matcher matcher, int group) {
String value = matcher.group(group);
return value == null ? 0 : Long.parseLong(value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package org.evomaster.client.java.utils;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class IsoDurationParserTest {

private static void assertDuration(IsoDuration actual, long years, long months, long weeks, long days,
long hours, long minutes, long seconds) {
assertEquals(years, actual.years);
assertEquals(months, actual.months);
assertEquals(weeks, actual.weeks);
assertEquals(days, actual.days);
assertEquals(hours, actual.hours);
assertEquals(minutes, actual.minutes);
assertEquals(seconds, actual.seconds);
}

// -------------------------------------------------------------------------
// General format
// -------------------------------------------------------------------------

@Test
void generalFormat_dateOnly_parsesYearsMonthsDays() {
assertDuration(IsoDurationParser.parse("P1Y2M3D"), 1, 2, 0, 3, 0, 0, 0);
}

@Test
void generalFormat_timeOnly_parsesHoursMinutesSeconds() {
assertDuration(IsoDurationParser.parse("PT4H5M6S"), 0, 0, 0, 0, 4, 5, 6);
}

@Test
void generalFormat_dateAndTimeCombined_parsesAllComponents() {
assertDuration(IsoDurationParser.parse("P1Y2M3DT4H5M6S"), 1, 2, 0, 3, 4, 5, 6);
}

@Test
void generalFormat_lowercasePrefix_isAccepted() {
assertDuration(IsoDurationParser.parse("p1y2m3d"), 1, 2, 0, 3, 0, 0, 0);
}

@Test
void generalFormat_barePrefix_parsesAsZeroDuration() {
// Mirrors the driver's own grammar, where every component is optional: a bare "P" is a
// valid, if degenerate, zero-length duration rather than an error.
assertDuration(IsoDurationParser.parse("P"), 0, 0, 0, 0, 0, 0, 0);
}

@Test
void generalFormat_bareTimeDesignatorWithNoValue_parsesAsZeroDuration() {
assertDuration(IsoDurationParser.parse("PT"), 0, 0, 0, 0, 0, 0, 0);
}

@Test
void generalFormat_dateOnlyWithBareTrailingTimeDesignator_parsesDateComponents() {
assertDuration(IsoDurationParser.parse("P1Y2M3DT"), 1, 2, 0, 3, 0, 0, 0);
}

@Test
void generalFormat_malformedContent_throwsIllegalArgumentException() {
assertThrows(IllegalArgumentException.class, () -> IsoDurationParser.parse("P1X"));
}

// -------------------------------------------------------------------------
// Weeks-only format
// -------------------------------------------------------------------------

@Test
void weekFormat_parsesWeeks() {
assertDuration(IsoDurationParser.parse("P3W"), 0, 0, 3, 0, 0, 0, 0);
}

@Test
void weekFormat_lowercasePrefix_isAccepted() {
assertDuration(IsoDurationParser.parse("p3w"), 0, 0, 3, 0, 0, 0, 0);
}

@Test
void weekFormat_mixedWithOtherUnits_throwsIllegalArgumentException() {
assertThrows(IllegalArgumentException.class, () -> IsoDurationParser.parse("P1Y2W"));
}

// -------------------------------------------------------------------------
// Alternative format
// -------------------------------------------------------------------------

@Test
void alternativeFormat_parsesAllComponents() {
assertDuration(IsoDurationParser.parse("P0001-02-03T04:05:06"), 1, 2, 0, 3, 4, 5, 6);
}

@Test
void alternativeFormat_lowercasePrefix_isAccepted() {
assertDuration(IsoDurationParser.parse("p0000-01-00T00:00:00"), 0, 1, 0, 0, 0, 0, 0);
}

@Test
void alternativeFormat_missingTimePart_throwsIllegalArgumentException() {
assertThrows(IllegalArgumentException.class, () -> IsoDurationParser.parse("P2024-01"));
}

// -------------------------------------------------------------------------
// Errors
// -------------------------------------------------------------------------

@Test
void parse_notPrefixed_throwsIllegalArgumentException() {
assertThrows(IllegalArgumentException.class, () -> IsoDurationParser.parse("3d"));
}
}
5 changes: 5 additions & 0 deletions client-java/controller/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,11 @@
<artifactId>libthrift</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.cassandra</groupId>
<artifactId>java-driver-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>bson</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package org.evomaster.client.java.controller.cassandra.calculator;

import org.evomaster.client.java.controller.cassandra.model.CassandraRow;
import org.evomaster.client.java.controller.cassandra.operations.CqlQueryOperation;
import org.evomaster.client.java.controller.cassandra.parser.CqlParserUtils;
import org.evomaster.client.java.distance.heuristics.DistanceHelper;
import org.evomaster.client.java.distance.heuristics.Truthness;
import org.evomaster.client.java.distance.heuristics.TruthnessUtils;

import java.util.ArrayList;
import java.util.List;

import static org.evomaster.client.java.distance.heuristics.TruthnessUtils.FALSE_TRUTHNESS;
import static org.evomaster.client.java.distance.heuristics.TruthnessUtils.TRUE_TRUTHNESS;

/**
* Calculator designed to compute a branch-distance-based heuristic for a CQL (Cassandra Query Language) query,
* given the rows returned by executing that query against the database.
* <p>
* The distance reflects how far the query's WHERE condition is from being satisfied by
* the available data and is used to guide the search towards inserting data in the database
* that makes the executed queries return non-empty results during the execution of generated tests.
*/
public class CassandraHeuristicsCalculator {

private final CassandraOperationEvaluator evaluator = new CassandraOperationEvaluator();

/**
* Computes the heuristic distance of the given CQL query with respect to the provided rows.
* A distance of {@code 0.0} means the query is satisfied, while values closer
* to {@code 1.0} indicate the query is further from being satisfied.
*
* @param cqlQuery the CQL query to evaluate
* @param returnedRows all rows in the table the query targets, used to evaluate how close
* the query's condition is to matching at least one row
* @return a distance value in {@code [0.0, 1.0]}, where {@code 0.0} represents the best case
*/
public double computeDistance(String cqlQuery, Iterable<CassandraRow> returnedRows) {
return 1.0 - computeHQuery(cqlQuery, returnedRows).getOfTrue();
}

private Truthness computeHQuery(String cqlQuery, Iterable<CassandraRow> returnedRows) {
if (!CqlParserUtils.canParseCqlCommand(cqlQuery)) {
return FALSE_TRUTHNESS;
}

List<CassandraRow> rows = toList(returnedRows);

org.evomaster.client.java.controller.cassandra.parser.CqlParser.RootContext root =
CqlParserUtils.parseCqlCommand(cqlQuery);
CqlQueryOperation whereOp = CqlParserUtils.getWhereOperation(root);

Truthness hRowSet = computeHRowSet(rows);

if (whereOp == null) {
return hRowSet;
}

Truthness hCondition = computeHCondition(whereOp, rows);
return TruthnessUtils.buildAndAggregationTruthness(hRowSet, hCondition);
}

private Truthness computeHRowSet(List<CassandraRow> rows) {
return TruthnessUtils.getTruthnessToEmpty(rows.size()).invert();
}

private Truthness computeHCondition(CqlQueryOperation condition,
List<CassandraRow> rows) {
if (rows.isEmpty()) {
return FALSE_TRUTHNESS;
}

double maxOfTrue = 0.0;
for (CassandraRow row : rows) {
double ofTrue = evaluator.evaluate(condition, row).getOfTrue();
if (ofTrue >= 1.0) {
return TRUE_TRUTHNESS;
} else if (ofTrue > maxOfTrue) {
maxOfTrue = ofTrue;
}
}

return TruthnessUtils.buildScaledTruthness(DistanceHelper.C, maxOfTrue);
}

private static List<CassandraRow> toList(Iterable<CassandraRow> iterable) {
if (iterable instanceof List) {
return (List<CassandraRow>) iterable;
}

List<CassandraRow> list = new ArrayList<>();
for (CassandraRow row : iterable) {
list.add(row);
}
return list;
}
}
Loading
Loading