Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
c293276
Add test cases for MongoDB semantics not reproduced by the heuristics…
LautaroPetaccio Sep 7, 2026
80d5c69
Refactor `$all` heuristic evaluation for improved handling of edge ca…
jgaleotti Sep 8, 2026
65fbb12
Enhance `$all` heuristic evaluation to handle null and scalar field e…
jgaleotti Sep 8, 2026
82c0e7c
Add comprehensive bitmask support for MongoDB queries and operations
jgaleotti Sep 8, 2026
a07a0e3
Add `MongoUtils` utility for integral number handling; integrate with…
jgaleotti Sep 8, 2026
c435d3b
Refactor number comparison logic in Mongo heuristics to handle NaN ca…
jgaleotti Sep 8, 2026
42ee0b5
Refactor heuristic calculation to support scalar-to-list comparisons;…
jgaleotti Sep 8, 2026
16dfb46
Refactor `$nin` heuristic evaluation; re-enable and extend related te…
jgaleotti Sep 8, 2026
f99a982
Refactor heuristic evaluation logic for `$in` and `$nin` operations
jgaleotti Sep 8, 2026
b876be3
Refactor `$regex` heuristic evaluation to handle array fields
jgaleotti Sep 8, 2026
b499cdb
Refactor Mongo heuristic comparison to remove unsupported type checks…
jgaleotti Sep 8, 2026
820b48c
Re-enable test for `$ne` between incomparable BSON types; remove outd…
jgaleotti Sep 8, 2026
445e453
fix parsing logic for `$not` with multiple comparison operators
jgaleotti Sep 8, 2026
ca4d299
Refactor `$not` heuristic calculation; remove field existence check a…
jgaleotti Sep 8, 2026
ce4f804
Refactor `TrueOperation` to `EmptyOperation`; update query parsing lo…
jgaleotti Sep 8, 2026
9282ade
Refactor heuristic calculation to handle arrays in comparison operations
jgaleotti Sep 8, 2026
281b509
Merge remote-tracking branch 'origin/master' into fix_all_semantics
jgaleotti Sep 9, 2026
afe42f4
Add additional unit tests for Mongo heuristic operations and refactor…
jgaleotti Sep 11, 2026
eae56b1
Refactor Mongo heuristic calculations: extract helper class, simplify…
jgaleotti Sep 11, 2026
617861e
Refactor Mongo heuristic calculations: extract helper class, simplify…
jgaleotti Sep 11, 2026
236ab8e
Merge remote-tracking branch 'origin/master' into fix_all_semantics
jgaleotti Sep 11, 2026
be1815d
Rename methods in `MongoHeuristicsCalculatorHelper` for improved clar…
jgaleotti Sep 12, 2026
9f73d5a
Merge branch 'master' into fix_all_semantics
LautaroPetaccio Sep 14, 2026
02c4534
Add tests for Mongo semantics the heuristics calculator does not repr…
LautaroPetaccio Sep 14, 2026
569aa59
Merge remote-tracking branch 'origin/master' into fix_all_semantics
jgaleotti Sep 16, 2026
69c947a
Merge pull request #1762 from WebFuzzing/tests/mongo-heuristics-seman…
jgaleotti Sep 16, 2026
bba0b74
$mod with zero divisor is not accepted as a valid MongoDB query anymore.
jgaleotti Sep 17, 2026
8791c29
evaluate $regex operation vs. an array checks that at least one eleme…
jgaleotti Sep 17, 2026
3217b2f
remove //@Disabled
jgaleotti Sep 17, 2026
f48d59f
$eq checks for equality of List or elemens when comparing two lists.
jgaleotti Sep 17, 2026
e9ce235
$all semantics is re-implemented as an $and of $eq operations.
jgaleotti Sep 18, 2026
49344be
refactor: left/right renamed to expected/actual
jgaleotti Sep 18, 2026
01e1a82
refactor: renaming to follow semantics
jgaleotti Sep 18, 2026
959df33
add support for parsing org.bson.types.Binary and org.bson.BsonBinary
jgaleotti Sep 18, 2026
2549358
remove @Disabled for passing test
jgaleotti Sep 18, 2026
7617de6
remove @Disabled for passing test
jgaleotti Sep 18, 2026
b48cc8a
add support of $eq and $ne comparisons among Binary values
jgaleotti Sep 18, 2026
f7b6c43
add TODO and exception message when $lte, $gte, $gt and $gte on byte[…
jgaleotti Sep 18, 2026
bce0256
$lte, $gte, $gt and $gte queries can accept null values / fix semanti…
jgaleotti Sep 18, 2026
1646023
Merge remote-tracking branch 'origin/master' into fix_all_semantics
jgaleotti Sep 18, 2026
bdc1448
comparison operations for BsonRegularExpression data type
jgaleotti Sep 18, 2026
848f00d
fix comparison to {"a": {"$not": /x/}}
jgaleotti Sep 18, 2026
2ca76a5
fix semantics for $eq, $lte, $gt, $gte, $ne of documents
jgaleotti Sep 18, 2026
0797c2e
fix semantics for $eq, $lte, $gt, $gte, $ne of ObjectIds
jgaleotti Sep 18, 2026
640be3b
removed old semantics test case
jgaleotti Sep 19, 2026
8daaddd
restore disabled test case
jgaleotti Sep 19, 2026
42d72f5
support for $type operations with a list of types / support for "numb…
jgaleotti Sep 21, 2026
747e5f1
fix test cases
jgaleotti Sep 21, 2026
aff379a
fix parsing of MongoDB queries. Now negative/fractional bitmasks are …
jgaleotti Sep 22, 2026
5d15c36
fix parsing of $exists queries for non-boolean values (e.g. falsy and…
jgaleotti Sep 22, 2026
4c20654
traverse documents only once (allows OneShotIterable)
jgaleotti Sep 22, 2026
ed35938
fixed issue with $comment
jgaleotti Sep 22, 2026
92efc62
an $in operator matches a regex in the expected value
jgaleotti Sep 22, 2026
be05a10
fix $elemMatch within $all condition
jgaleotti Sep 22, 2026
9d53c61
Merge remote-tracking branch 'origin/master' into fix_all_semantics
jgaleotti Sep 22, 2026
5bbf6bb
added test case for $elemMatch within $all
jgaleotti Sep 22, 2026
d0a58bd
removed comments
jgaleotti Sep 22, 2026
6fdafaf
Merge remote-tracking branch 'origin/master' into fix_all_semantics
jgaleotti Sep 23, 2026
61ac4d3
specify visibility and documentation
jgaleotti Sep 23, 2026
c61e044
refactor moved utils to TruthnessUtils
jgaleotti Sep 23, 2026
d4e9c77
fix test case
jgaleotti Sep 23, 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

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import org.evomaster.client.java.controller.mongo.operations.QueryOperationWithField;
import org.evomaster.client.java.controller.mongo.selectors.*;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
Expand All @@ -19,6 +21,10 @@
public class QueryParser {

private static final String SYNTHETIC_FIELD_NAME = "$";
private static final String COMMENT_OPERATOR = "$comment";
// "$comments" is not a real MongoDB operator: it is treated as noise/metadata to be
// stripped wherever it appears, unlike "$comment" which is the real comment operator.
private static final Set<String> COMMENTS_OPERATORS = new HashSet<>(Arrays.asList("$comments"));

List<QuerySelector> selectors = Arrays.asList(
new EqualsSelector(),
Expand Down Expand Up @@ -54,18 +60,76 @@ public QueryOperation parse(Object bsonDocument) {
return null;
}

QueryOperation operation = parseWithSelectors(bsonDocument);
// A document made up entirely of "$comments" noise, with nothing else to query on,
// does not represent any real condition.
if (isOnlyNoiseComments(bsonDocument)) {
return null;
}

// "$comments" is noise and gets stripped wherever it appears, however deeply nested,
// since it never carries query semantics. "$comment" is the real MongoDB operator: it
// only has meaning as a predicate-level key (this document's own keys), so it is
// stripped shallowly here and left untouched inside field values/literals (e.g. under
// an explicit $eq, or as the entire value of a field), where it must be compared as-is.
Object normalizedWithoutComments = removeTopLevelCommentOperator(removeCommentsOperators(bsonDocument));

QueryOperation operation = parseWithSelectors(normalizedWithoutComments);
if (operation != null && !usesOperatorAsFieldName(operation)) {
return operation;
}

Object normalizedDocument = normalizeTopLevelValueOperatorQuery(bsonDocument);
if (normalizedDocument == bsonDocument) {
Object normalizedDocument = normalizeTopLevelValueOperatorQuery(normalizedWithoutComments);
if (normalizedDocument == normalizedWithoutComments) {
return operation;
}
return parseWithSelectors(normalizedDocument);
}
Comment thread
jgaleotti marked this conversation as resolved.

private boolean isOnlyNoiseComments(Object bsonDocument) {
if (!isBsonDocument(bsonDocument)) {
return false;
}
Set<String> keys = documentKeys(bsonDocument);
return keys != null && !keys.isEmpty() && keys.stream().allMatch(COMMENTS_OPERATORS::contains);
}

private Object removeTopLevelCommentOperator(Object bsonValue) {
if (!isBsonDocument(bsonValue)) {
return bsonValue;
}
Object normalized = newDocument(bsonValue);
for (String key : documentKeys(bsonValue)) {
if (key.equals(COMMENT_OPERATOR)) {
continue;
}
appendToDocument(normalized, key, getValue(bsonValue, key));
}
return normalized;
}

private Object removeCommentsOperators(Object bsonValue) {
if (isBsonDocument(bsonValue)) {
Object normalized = newDocument(bsonValue);
for (String key : documentKeys(bsonValue)) {
if (COMMENTS_OPERATORS.contains(key)) {
continue;
}
appendToDocument(normalized, key, removeCommentsOperators(getValue(bsonValue, key)));
}
return normalized;
}

if (bsonValue instanceof List<?>) {
List<Object> normalized = new ArrayList<>();
for (Object item : (List<?>) bsonValue) {
normalized.add(removeCommentsOperators(item));
}
return normalized;
}

return bsonValue;
}

private boolean usesOperatorAsFieldName(QueryOperation operation) {
if (!(operation instanceof QueryOperationWithField)) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
* This query matches all documents in a collection, effectively
* returning all documents without any filtering.
*/
public class TrueOperation extends QueryOperation {
public class EmptyOperation extends QueryOperation {

public TrueOperation() {
public EmptyOperation() {

}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.evomaster.client.java.controller.mongo.operations;

import java.util.Objects;
import java.util.regex.Pattern;

/**
* Options supported by MongoDB's {@code $regex} operator.
Expand Down Expand Up @@ -87,4 +88,30 @@ public boolean equals(Object other) {
public int hashCode() {
return Objects.hash(caseInsensitive, multiline, dotAll, extended, unicode);
}

public int toJavaRegexFlags() {
int flags = 0;
if (isCaseInsensitive()) {
flags |= Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE;
}
if (isMultiline()) {
flags |= Pattern.MULTILINE;
}
if (isDotAll()) {
flags |= Pattern.DOTALL;
}
if (isExtended()) {
flags |= Pattern.COMMENTS;
}
if (isUnicode()) {
/**
* MongoDB's 'u' option is intentionally mapped to no Java regex flag.
* For MongoDB $regex, UTF mode is enabled by default, making 'u' redundant.
* Java regex operates on Unicode strings, so no equivalent Pattern flag is required.
*/
}
return flags;
}


}
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
package org.evomaster.client.java.controller.mongo.operations;


import org.evomaster.client.java.controller.mongo.utils.BsonHelper;

import java.util.List;
import java.util.Objects;

/**
* Represent $type operation.
* Selects documents where the value of the field is an instance of the specified BSON type(s).
*/
public class TypeOperation extends QueryOperationWithField {
private final Object type;
private final List<Object> bsonTypes;

public TypeOperation(String fieldName, Object type) {
public TypeOperation(String fieldName, List<Object> bsonTypes) {
super(fieldName);
this.type = type;
Objects.requireNonNull(bsonTypes);
if (bsonTypes.isEmpty()) {
throw new IllegalArgumentException("BSON types list cannot be empty");
}
for (Object type : bsonTypes) {
if (!BsonHelper.isBsonType(type)) {
throw new IllegalArgumentException("Invalid BSON type: " + type);
}
}
this.bsonTypes = bsonTypes;
}

public Object getType() {
return type;
public List<Object> getBsonTypes() {
return bsonTypes;
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
package org.evomaster.client.java.controller.mongo.selectors;

import org.evomaster.client.java.controller.mongo.QueryParser;
import org.evomaster.client.java.controller.mongo.operations.*;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;

import static org.evomaster.client.java.controller.mongo.utils.BsonHelper.documentKeys;
import static org.evomaster.client.java.controller.mongo.utils.BsonHelper.getValue;
import static org.evomaster.client.java.controller.mongo.utils.BsonHelper.isBsonDocument;

/**
* Represents a selector for the MongoDB `$all` operator.
Expand All @@ -13,23 +20,50 @@
* This operator matches arrays that contain all elements specified in the query.
* The selector checks if the query value is a list and, if so, creates an
* {@link AllOperation} object corresponding to the field and the list of values.
* Elements shaped as { $elemMatch: {...} } are a documented exception: rather than
* being compared for equality, they are parsed into an {@link ElemMatchOperation}
* that must be satisfied by some element of the array.
*/
public class AllSelector extends SingleConditionQuerySelector {

public static final String ALL_OPERATOR = "$all";
private static final String ELEM_MATCH_OPERATOR = "$elemMatch";

@Override
protected QueryOperation parseValue(String fieldName, Object value) {
Objects.requireNonNull(fieldName);
Objects.requireNonNull(value);

if (value instanceof List<?>) {
return new AllOperation<>(fieldName, (List<?>) value);
List<Object> parsedValues = new ArrayList<>();
for (Object element : (List<?>) value) {
parsedValues.add(parseElement(fieldName, element));
}
return new AllOperation<>(fieldName, parsedValues);
} else {
return null;
}
}

private Object parseElement(String fieldName, Object element) {
if (!isBsonDocument(element)) {
return element;
}

Set<String> keys = documentKeys(element);
if (keys == null || keys.size() != 1 || !keys.contains(ELEM_MATCH_OPERATOR)) {
return element;
}

Object innerQuery = getValue(element, ELEM_MATCH_OPERATOR);
if (!isBsonDocument(innerQuery)) {
return element;
}

QueryOperation condition = new QueryParser().parse(innerQuery);
return condition == null ? element : new ElemMatchOperation(fieldName, condition);
}

@Override
protected String operator() {
return ALL_OPERATOR;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.evomaster.client.java.controller.mongo.operations.BitsAllClearOperation;
import org.evomaster.client.java.controller.mongo.operations.QueryOperation;
import org.evomaster.client.java.controller.mongo.utils.BitmaskUtils;

import java.util.Objects;
import java.util.OptionalLong;

/**
* { field: { $bitsAllClear: value } }
Expand All @@ -15,7 +17,8 @@ public class BitsAllClearSelector extends SingleConditionQuerySelector {
@Override
protected QueryOperation parseValue(String fieldName, Object value) {
Objects.requireNonNull(fieldName);
return value instanceof Long ? new BitsAllClearOperation(fieldName, (Long) value) : null;
OptionalLong bitmaskValue = BitmaskUtils.toBitMaskValue(value);
return bitmaskValue.isPresent() ? new BitsAllClearOperation(fieldName, bitmaskValue.getAsLong()) : null;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.evomaster.client.java.controller.mongo.operations.BitsAllSetOperation;
import org.evomaster.client.java.controller.mongo.operations.QueryOperation;
import org.evomaster.client.java.controller.mongo.utils.BitmaskUtils;

import java.util.Objects;
import java.util.OptionalLong;

/**
* { field: { $bitsAllSet: value } }
Expand All @@ -15,7 +17,8 @@ public class BitsAllSetSelector extends SingleConditionQuerySelector {
@Override
protected QueryOperation parseValue(String fieldName, Object value) {
Objects.requireNonNull(fieldName);
return value instanceof Long ? new BitsAllSetOperation(fieldName, (Long) value) : null;
OptionalLong bitmaskValue = BitmaskUtils.toBitMaskValue(value);
return bitmaskValue.isPresent() ? new BitsAllSetOperation(fieldName, bitmaskValue.getAsLong()) : null;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.evomaster.client.java.controller.mongo.operations.BitsAnyClearOperation;
import org.evomaster.client.java.controller.mongo.operations.QueryOperation;
import org.evomaster.client.java.controller.mongo.utils.BitmaskUtils;

import java.util.Objects;
import java.util.OptionalLong;

/**
* { field: { $bitsAnyClear: value } }
Expand All @@ -15,7 +17,8 @@ public class BitsAnyClearSelector extends SingleConditionQuerySelector {
@Override
protected QueryOperation parseValue(String fieldName, Object value) {
Objects.requireNonNull(fieldName);
return value instanceof Long ? new BitsAnyClearOperation(fieldName, (Long) value) : null;
OptionalLong bitmaskValue = BitmaskUtils.toBitMaskValue(value);
return bitmaskValue.isPresent() ? new BitsAnyClearOperation(fieldName, bitmaskValue.getAsLong()) : null;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

import org.evomaster.client.java.controller.mongo.operations.BitsAnySetOperation;
import org.evomaster.client.java.controller.mongo.operations.QueryOperation;
import org.evomaster.client.java.controller.mongo.utils.BitmaskUtils;

import java.util.List;
import java.util.Objects;
import java.util.OptionalLong;

/**
* { field: { $bitsAnySet: value } }
Expand All @@ -15,9 +18,11 @@ public class BitsAnySetSelector extends SingleConditionQuerySelector {
@Override
protected QueryOperation parseValue(String fieldName, Object value) {
Objects.requireNonNull(fieldName);
return value instanceof Long ? new BitsAnySetOperation(fieldName, (Long) value) : null;
OptionalLong bitmaskValue = BitmaskUtils.toBitMaskValue(value);
return bitmaskValue.isPresent() ? new BitsAnySetOperation(fieldName, bitmaskValue.getAsLong()) : null;
}


@Override
protected String operator() {
return BITS_ANY_SET_OPERATOR;
Expand Down
Loading
Loading