From c293276bf88420205f8ec50c861f304c000a53c2 Mon Sep 17 00:00:00 2001 From: LautaroPetaccio Date: Mon, 7 Sep 2026 14:53:54 -0300 Subject: [PATCH 01/53] Add test cases for MongoDB semantics not reproduced by the heuristics calculator Twenty-four defects in MongoHeuristicsCalculator, each as its own @Disabled test, so they can be enabled one at a time and in any order as the behaviour is implemented. Every expected value was obtained by running the same query and the same document against a real MongoDB 7.0.40 server, so the assertions state what the database does rather than an interpretation of the documentation. Ten of them make the calculator throw. As MongoHandler does not catch anything, the exception escapes the heuristics computation for the action, so the ExtraHeuristicsDto is lost, including the SQL heuristics computed before it: $type with a string alias or with a list of aliases, a bitmask given as an Integer or as a list of bit positions, and $not holding more than one operator, are not parsed, and the calculator throws a NullPointerException on the resulting null operation; an operator that is not modelled at all is not parsed either, which covers $expr, $jsonSchema, $where, $text, $geoWithin, $geoIntersects and $comment. The last one attaches to an otherwise ordinary query, so {"a": 1, "$comment": "..."} is enough to lose the heuristics of the action; an empty list of values, an empty array in the document, and comparing two empty arrays aggregate over no element and throw IllegalArgumentException; an ordering comparison involving NaN builds a Truthness with neither of its values equal to 1, which its own constructor rejects. Any double field can hold NaN; a value the calculator cannot compare, such as a sub-document or binary data, reaches the "Unsupported type" branch and throws. The test for the operators that are not modelled asserts only that nothing is thrown, not any particular score: whether such an operator should be supported, and what it should answer, is a decision for the heuristic. What it should not do is cost the action its heuristics. The other fourteen are answered, but not the way MongoDB answers them. Those that report a match where MongoDB has none are the harmful direction, as a condition no data can satisfy is recorded as covered and the search stops working towards it. Several share one rule: MongoDB matches a field when its value satisfies the condition, or when it holds an array of which any element does, and that applies to every condition on a field rather than only to equality. $all is the same rule quantified the other way round, over the expected values. Four tests of behaviour that is already correct are added as well, as a guard while the heuristic is changed. One of them, testNotEqualsAgainstAnArrayField, answers correctly only because two of the reported defects cancel each other out; fixing either one alone turns it into a false positive, which is why it is worth keeping visible. --- .../mongo/MongoHeuristicsCalculatorTest.java | 517 ++++++++++++++++++ 1 file changed, 517 insertions(+) diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 50675c235e..9d1316dc86 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -5,6 +5,7 @@ import org.bson.codecs.DecoderContext; import org.bson.codecs.DocumentCodec; import org.bson.conversions.Bson; +import org.bson.types.Binary; import org.bson.types.Decimal128; import org.bson.types.ObjectId; import org.evomaster.client.java.controller.internal.db.mongo.MongoDistanceWithMetrics; @@ -1465,4 +1466,520 @@ public static Document convertToDocument(Bson filter) { return documentCodec.decode(bsonDocument.asBsonReader(), DecoderContext.builder().build()); } + + /* + ================================================================================ + MongoDB semantics: behaviour that is currently not reproduced by the calculator. + + Every expected value below was obtained by running the same query and the same + document against a real MongoDB 7.0.40 server, so the assertions state what the + database actually does rather than an interpretation of the documentation. + + The @Disabled tests fail today. Removing the annotation is all that is needed + once the corresponding behaviour is implemented, and they are deliberately one + per defect so they can be enabled independently, in any order. + + First, a few tests of behaviour that is already correct, as a guard while the + heuristic is being changed. + ================================================================================ + */ + + @Test + public void testOrderingComparisonsDoNotMatchAcrossIncomparableTypes() { + // mongo: {value:42} is matched by none of these, a number and a string do not compare + Document doc = new Document().append("value", 42); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + assertTrue(calculator.computeHeuristicDocument(convertToDocument(Filters.gt("value", "abc")), doc).isFalse()); + assertTrue(calculator.computeHeuristicDocument(convertToDocument(Filters.gte("value", "abc")), doc).isFalse()); + assertTrue(calculator.computeHeuristicDocument(convertToDocument(Filters.lt("value", "abc")), doc).isFalse()); + assertTrue(calculator.computeHeuristicDocument(convertToDocument(Filters.lte("value", "abc")), doc).isFalse()); + } + + @Test + public void testEqualsMatchingAnArrayFieldAsAWhole() { + // mongo: matches, an array is equal to another one with the same elements in order + Document doc = new Document().append("tags", new ArrayList<>(Arrays.asList("a", "b"))); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.eq("tags", Arrays.asList("a", "b"))), doc).isTrue()); + // mongo: no match, the order of the elements differs + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.eq("tags", Arrays.asList("b", "a"))), doc).isFalse()); + } + + @Test + public void testTypeWithANumericCodeAndBitsWithALongBitmask() { + // the two forms of these operators that are handled today, see the disabled tests below + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + Document stringField = new Document().append("a", "abc"); + Document typeQuery = new Document().append("a", new Document().append("$type", 2)); + assertTrue(calculator.computeHeuristicDocument(typeQuery, stringField).isTrue()); + + Document flags = new Document().append("a", 5); + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.bitsAllSet("a", 1L)), flags).isTrue()); + } + + @Test + public void testNotEqualsAgainstAnArrayField() { + /* + mongo: no match, the array holds 1. + + This one already answers correctly, but only because two of the defects below + cancel each other out: $ne does not look at the elements of the array, which on + its own would answer true, and the comparison of two values of incomparable BSON + types answers false regardless of the operator, which brings it back to false. + Fixing either one alone turns this into a false positive, so it is worth keeping + as it is while both are addressed. + */ + Document doc = new Document().append("a", new ArrayList<>(Arrays.asList(1, 2, 3))); + assertTrue(new MongoHeuristicsCalculator() + .computeHeuristicDocument(convertToDocument(Filters.ne("a", 1)), doc).isFalse()); + } + + /* + ================================================================================ + Queries that make the calculator throw. As MongoHandler does not catch anything, + the exception escapes the whole heuristics computation for the action, so the + ExtraHeuristicsDto is lost, including the SQL heuristics computed before it. + ================================================================================ + */ + + @Test + @Disabled("$type with a string alias is not parsed, so the calculator throws a NullPointerException") + public void testTypeWithAStringAlias() { + /* + mongo: {"a": {"$type": "string"}} matches {a:'abc'} and not {a:5}. + The string aliases are the documented form of this operator. Only the numeric + codes are parsed today, so the query parses to null and the calculator throws. + Note that {"$type": "array"} needs the value of the field to be compared as an + array, which is a second problem in the same operator: the check is made on the + name of the Java class, so an ArrayList is never a java.util.List. + */ + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.type("a", "string")), new Document().append("a", "abc")).isTrue()); + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.type("a", "string")), new Document().append("a", 5)).isFalse()); + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.type("a", "array")), + new Document().append("a", new ArrayList<>(Arrays.asList(1, 2)))).isTrue()); + } + + @Test + @Disabled("a bitmask given as an Integer is not parsed, so the calculator throws a NullPointerException") + public void testBitsWithAnIntegerBitmask() { + /* + mongo: matches, any integer bitmask is accepted. It arrives as an Integer + whenever the value fits in one, so this is an ordinary query. The selectors + accept only a Long, so the query parses to null and the calculator throws. + */ + Document doc = new Document().append("a", 5); + Document query = new Document().append("a", new Document().append("$bitsAllSet", 1)); + + assertTrue(new MongoHeuristicsCalculator().computeHeuristicDocument(query, doc).isTrue()); + } + + @Test + @Disabled("an empty list of values makes the calculator throw IllegalArgumentException") + public void testInAndNotInWithAnEmptyListOfValues() { + // mongo: {$in: []} matches nothing, {$nin: []} matches everything + Document doc = new Document().append("tags", new ArrayList<>(Arrays.asList("a", "b"))); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.in("tags", Collections.emptyList())), doc).isFalse()); + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.nin("tags", Collections.emptyList())), doc).isTrue()); + } + + @Test + @Disabled("an empty array in the document makes the calculator throw IllegalArgumentException") + public void testInAndNotInAgainstAnEmptyArrayField() { + // mongo: an empty array holds no value, so $in does not match and $nin does + Document doc = new Document().append("tags", Collections.emptyList()); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.in("tags", Arrays.asList("a"))), doc).isFalse()); + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.nin("tags", Arrays.asList("a"))), doc).isTrue()); + } + + @Test + @Disabled("comparing two empty arrays makes the calculator throw IllegalArgumentException") + public void testEqualsBetweenEmptyArrays() { + /* + mongo: matches, two empty arrays are equal. The two are of equal size, so the + comparison aggregates over their elements, of which there are none. + */ + Document doc = new Document().append("tags", Collections.emptyList()); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.eq("tags", Collections.emptyList())), doc).isTrue()); + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.ne("tags", Collections.emptyList())), doc).isFalse()); + } + + @Test + @Disabled("a value the calculator cannot compare makes it throw IllegalArgumentException") + public void testFieldsHoldingASubDocument() { + /* + mongo: does not fail on any of these, it simply does not match. A sub-document + reaches the "Unsupported type" branch instead, which throws. Arrays of + sub-documents are the common case of this. + */ + Document subDocument = new Document().append("a", new Document().append("x", 1)); + Document arrayOfSubDocuments = new Document().append("a", + new ArrayList<>(Arrays.asList(new Document().append("x", 1)))); + Document binaryData = new Document().append("a", new Binary(new byte[]{1, 2})); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + assertTrue(calculator.computeHeuristicDocument(convertToDocument(Filters.eq("a", 5)), subDocument).isFalse()); + assertTrue(calculator.computeHeuristicDocument(convertToDocument(Filters.gt("a", 2)), subDocument).isFalse()); + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.in("a", Arrays.asList(1, 5))), arrayOfSubDocuments).isFalse()); + // mongo: matches, a sub-document is different from a number + assertTrue(calculator.computeHeuristicDocument(convertToDocument(Filters.ne("a", 5)), subDocument).isTrue()); + + // binary data is the other value this calculator has no comparison for + assertTrue(calculator.computeHeuristicDocument(convertToDocument(Filters.eq("a", 5)), binaryData).isFalse()); + } + + /* + ================================================================================ + Queries that are answered, but not the way MongoDB answers them. The ones that + report a match where MongoDB has none are the harmful direction: a condition no + data can satisfy is recorded as covered, so the search stops working towards it. + ================================================================================ + */ + + @Test + @Disabled("$all is quantified over the elements of the document instead of the expected values") + public void testAllQuantifiesOverTheExpectedValues() { + /* + $all holds when every expected value is present in the array, and is + indifferent to any further element. The two readings coincide only when the + two lists are equal, which is the case the existing testAll covers. + */ + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + // mongo: matches, "a" is present and the extra "b" is irrelevant + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.all("tags", Arrays.asList("a"))), + new Document().append("tags", new ArrayList<>(Arrays.asList("a", "b")))).isTrue()); + + // mongo: no match, "b" and "c" are missing. Reported as fully satisfied today + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.all("tags", Arrays.asList("a", "b", "c"))), + new Document().append("tags", new ArrayList<>(Arrays.asList("a")))).isFalse()); + } + + @Test + @Disabled("$ne between values of incomparable BSON types is reported as false") + public void testNotEqualsBetweenIncomparableTypes() { + // mongo: matches, a number and a string are different from each other + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.ne("value", "abc")), new Document().append("value", 42)).isTrue()); + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.ne("value", 1)), new Document().append("value", true)).isTrue()); + } + + @Test + @Disabled("$nin does not look at the elements of an array-valued field") + public void testNotInAgainstAnArrayField() { + Document doc = new Document().append("tags", new ArrayList<>(Arrays.asList("a", "b"))); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + // mongo: no match, the array holds "a", which is excluded. Reported as satisfied today + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.nin("tags", Arrays.asList("a"))), doc).isFalse()); + // mongo: matches, the array holds neither of the excluded values + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.nin("tags", Arrays.asList("z"))), doc).isTrue()); + } + + @Test + @Disabled("$not answers true whenever the field is absent, overriding the inner operator") + public void testNotOnAMissingFieldWithAnInnerOperatorThatMatchesIt() { + /* + mongo: no match for any of these. $ne, $nin and $exists:false all match a + document in which the field is absent, so negating them must not. + */ + Document doc = new Document().append("name", "Bob"); // "age" is absent + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.not(Filters.ne("age", 5))), doc).isFalse()); + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.not(Filters.nin("age", Arrays.asList(1, 2)))), doc).isFalse()); + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.not(Filters.exists("age", false))), doc).isFalse()); + } + + @Test + @Disabled("a field is matched only as a whole, never by an element of the array it holds") + public void testFieldsAreMatchedByAnElementOfTheArrayTheyHold() { + /* + mongo matches all of these. A field holding an array satisfies a condition when + the array itself does, or when any one of its elements does, and this applies to + every condition on a field rather than only to equality. + */ + Document doc = new Document().append("a", new ArrayList<>(Arrays.asList(1, 2, 3))); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + assertTrue(calculator.computeHeuristicDocument(convertToDocument(Filters.eq("a", 1)), doc).isTrue()); + assertTrue(calculator.computeHeuristicDocument(convertToDocument(Filters.gt("a", 2)), doc).isTrue()); + assertTrue(calculator.computeHeuristicDocument(convertToDocument(Filters.gte("a", 3)), doc).isTrue()); + assertTrue(calculator.computeHeuristicDocument(convertToDocument(Filters.lt("a", 2)), doc).isTrue()); + assertTrue(calculator.computeHeuristicDocument(convertToDocument(Filters.lte("a", 1)), doc).isTrue()); + assertTrue(calculator.computeHeuristicDocument(convertToDocument(Filters.mod("a", 2L, 1L)), doc).isTrue()); + assertTrue(calculator.computeHeuristicDocument(convertToDocument(Filters.bitsAllSet("a", 1L)), doc).isTrue()); + + // mongo: no match, and negating a comparison over an array is where this turns into + // a false positive today + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.not(Filters.gt("a", 2))), doc).isFalse()); + } + + @Test + @Disabled("$all on a field that does not hold an array is reported as false") + public void testAllOnAScalarField() { + // mongo: a scalar is matched by a $all listing only values equal to it + Document doc = new Document().append("tag", "a"); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.all("tag", Arrays.asList("a"))), doc).isTrue()); + // mongo: no match, a scalar cannot be equal to both + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.all("tag", Arrays.asList("a", "b"))), doc).isFalse()); + } + + @Test + @Disabled("$in and $nin compare only the elements of an array, never the array itself") + public void testInMatchingAnArrayFieldAsAWhole() { + Document doc = new Document().append("tags", new ArrayList<>(Arrays.asList("a", "b"))); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + // mongo: matches, one of the listed values is the array itself + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.in("tags", Arrays.asList(Arrays.asList("a", "b")))), doc).isTrue()); + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.nin("tags", Arrays.asList(Arrays.asList("a", "b")))), doc).isFalse()); + } + + @Test + @Disabled("a bitwise operator truncates a non-integral number instead of not matching it") + public void testBitsDoesNotMatchANonIntegralNumber() { + // mongo: 3.0 is an integer and is matched, 3.5 is not truncated to 3 + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + Bson query = Filters.bitsAllSet("a", 1L); + + assertTrue(calculator.computeHeuristicDocument(convertToDocument(query), + new Document().append("a", 3.0d)).isTrue()); + assertTrue(calculator.computeHeuristicDocument(convertToDocument(query), + new Document().append("a", 3.5d)).isFalse()); + } + + @Test + @Disabled("$elemMatch with a condition on a field matches an array of scalars") + public void testElemMatchOnAnArrayOfScalars() { + // mongo: no match, no element of the array is a document holding "x" + Document doc = new Document().append("a", new ArrayList<>(Arrays.asList(1, 2, 3))); + assertTrue(new MongoHeuristicsCalculator().computeHeuristicDocument( + convertToDocument(Filters.elemMatch("a", Filters.eq("x", 1))), doc).isFalse()); + } + + @Test + @Disabled("a dotted field path is never resolved into the sub-document it names") + public void testDottedFieldPath() { + // mongo: matches both, "a.x" names the field "x" of the sub-document held by "a" + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + assertTrue(calculator.computeHeuristicDocument(convertToDocument(Filters.eq("a.x", 1)), + new Document().append("a", new Document().append("x", 1))).isTrue()); + assertTrue(calculator.computeHeuristicDocument(convertToDocument(Filters.eq("a.x", 1)), + new Document().append("a", new ArrayList<>(Arrays.asList( + new Document().append("x", 1), new Document().append("x", 2))))).isTrue()); + } + + /* + ================================================================================ + Cases about the BSON value types a field can hold, and about the operators that + are not modelled at all. Same convention: the expected values come from a real + MongoDB 7.0.40 server. + ================================================================================ + */ + + @Test + @Disabled("an ordering comparison involving NaN makes the calculator throw IllegalArgumentException") + public void testOrderingComparisonWithNaN() { + /* + mongo: no match for either, NaN does not order against anything. + Both "less than" and "greater or equal" are false for NaN, so the Truthness + built for the comparison has neither of its two values equal to 1 and its own + constructor rejects it with "At least one value should be equal to 1". + NaN is reachable in ordinary data, as any double field can hold it. + */ + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + assertTrue(calculator.computeHeuristicDocument(convertToDocument(Filters.gt("a", 5.0d)), + new Document().append("a", Double.NaN)).isFalse()); + assertTrue(calculator.computeHeuristicDocument(convertToDocument(Filters.lt("a", Double.NaN)), + new Document().append("a", 5)).isFalse()); + } + + @Test + @Disabled("an ObjectId is compared against any other value as a string, which orders it wrongly") + public void testOrderingComparisonBetweenAnObjectIdAndANumber() { + /* + mongo: no match, an ObjectId and a number are of different BSON types and do + not order against each other. The comparison turns both into strings instead, + so "507f1f77bcf86cd799439011" is greater than "5.0" and the answer is a match. + */ + Document doc = new Document().append("a", new ObjectId("507f1f77bcf86cd799439011")); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + assertTrue(calculator.computeHeuristicDocument(convertToDocument(Filters.gt("a", 5.0d)), doc).isFalse()); + assertTrue(calculator.computeHeuristicDocument(convertToDocument(Filters.lt("a", 5.0d)), doc).isFalse()); + } + + @Test + @Disabled("null held as an element of an array is not matched") + public void testNullInsideAnArrayField() { + /* + mongo: {"a": {"$eq": null}} matches, as one element of the array is null, and + {"$ne": null} therefore does not. The second is the harmful direction, since it + is answered as a match today. + */ + Document doc = new Document().append("a", Arrays.asList(1, null, 3)); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.eq("a", null)), doc).isTrue()); + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.ne("a", null)), doc).isFalse()); + } + + @Test + @Disabled("$type with a list of aliases is not parsed, so the calculator throws a NullPointerException") + public void testTypeWithAListOfAliases() { + // mongo: matches, the field holds one of the listed types + Document query = new Document().append("a", + new Document().append("$type", Arrays.asList("string", "int"))); + + assertTrue(new MongoHeuristicsCalculator() + .computeHeuristicDocument(query, new Document().append("a", "abc")).isTrue()); + } + + @Test + @Disabled("$bits with a list of bit positions is not parsed, so the calculator throws a NullPointerException") + public void testBitsWithBitPositions() { + // mongo: matches, bits 0 and 2 are set in 5. The bitmask can be given as positions + Document query = new Document().append("a", + new Document().append("$bitsAllSet", Arrays.asList(0, 2))); + + assertTrue(new MongoHeuristicsCalculator() + .computeHeuristicDocument(query, new Document().append("a", 5)).isTrue()); + } + + @Test + @Disabled("an operator this calculator does not model makes it throw, instead of degrading") + public void testOperatorsThatAreNotModelledDoNotThrow() { + /* + None of these is parsed, and the calculator then throws a NullPointerException + on the resulting null operation. As MongoHandler does not catch anything, that + takes the whole ExtraHeuristicsDto for the action with it, including the SQL + heuristics computed before it. + + The point of this test is not that these operators should be supported. It is + that a query using one of them should not cost the action its heuristics: what + the score ought to be for an operator that is not modelled is a decision for + the heuristic, so the assertion here is only that nothing is thrown. + + $comment is worth singling out: it attaches to an otherwise ordinary query, so + {"a": 1, "$comment": "..."} is enough to lose the heuristics of the action. + */ + Document doc = new Document().append("a", 1); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + List queries = Arrays.asList( + new Document().append("a", 1).append("$comment", "a comment"), + new Document().append("$expr", + new Document().append("$eq", Arrays.asList("$a", 1))), + new Document().append("$jsonSchema", + new Document().append("required", Arrays.asList("a"))), + new Document().append("$where", "function(){ return true; }"), + new Document().append("$text", new Document().append("$search", "x")), + new Document().append("a", new Document().append("$geoWithin", + new Document().append("$centerSphere", + Arrays.asList(Arrays.asList(1.0, 2.0), 0.1)))), + new Document().append("a", new Document().append("$geoIntersects", + new Document().append("$geometry", + new Document().append("type", "Point") + .append("coordinates", Arrays.asList(1.0, 2.0))))) + ); + + for (Document query : queries) { + assertDoesNotThrow(() -> calculator.computeHeuristicDocument(query, doc), + "should not throw for " + query.toJson()); + } + } + + /* + ================================================================================ + Cases about $regex, and about the shapes $not can take. Same convention: the + expected values come from a real MongoDB 7.0.40 server. + ================================================================================ + */ + + @Test + @Disabled("$regex does not look at the elements of an array-valued field") + public void testRegexMatchingAnElementOfAnArrayField() { + /* + mongo: matches, one element of the array matches the expression. $regex follows + the same rule as the other operators on a field, which it does not apply today. + */ + Document doc = new Document().append("a", new ArrayList<>(Arrays.asList("abc", "x"))); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.regex("a", "^ab")), doc).isTrue()); + // mongo: no match, neither element matches + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.regex("a", "^zz")), doc).isFalse()); + } + + @Test + @Disabled("$not holding more than one operator is not parsed, so the calculator throws a NullPointerException") + public void testNotWithMoreThanOneInnerOperator() { + /* + mongo: {"a": {"$not": {"$gt": 1, "$lt": 9}}} matches {a:1}, as the two inner + conditions are combined with an implicit and, which is false here, and $not + negates it. + + The selector gives up whenever the inner expression parses to more than one + condition, and on a $not nested inside another $not, which MongoDB accepts as + well. In both cases the query parses to null and the calculator throws on it. + */ + Document doc = new Document().append("a", 1); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + Document twoOperators = new Document().append("a", + new Document().append("$not", new Document().append("$gt", 1).append("$lt", 9))); + assertTrue(calculator.computeHeuristicDocument(twoOperators, doc).isTrue()); + + // mongo: no match, the two negations cancel and 1 is not greater than 1 + Document nested = new Document().append("a", + new Document().append("$not", new Document().append("$not", new Document().append("$gt", 1)))); + assertTrue(calculator.computeHeuristicDocument(nested, doc).isFalse()); + } } From 80d5c69354284539c27c00cc2d2fd15b6c171bd6 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Tue, 8 Sep 2026 12:32:38 -0300 Subject: [PATCH 02/53] Refactor `$all` heuristic evaluation for improved handling of edge cases; re-enable disabled tests --- .../mongo/MongoHeuristicsCalculator.java | 32 +++++++++---------- .../mongo/MongoHeuristicsCalculatorTest.java | 2 -- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java index bfc6c52ff5..4ede4d403a 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java @@ -172,7 +172,7 @@ private Truthness computeHeuristic(RegexOperation operation, Object document) { final Pattern pattern = operation.getPattern(); final String patternString = pattern.pattern(); - if (taintHandler!=null) { + if (taintHandler != null) { final int patternFlags = pattern.flags(); // TODO: tainting should take into account the pattern flags, which can change the matching behavior // TODO: regex could be a partial word match (MongoDB $regex) instead of a whole word match (Matcher.matches()) @@ -537,26 +537,24 @@ private Truthness computeHeuristic(AllOperation operation, Object document) { List expectedValues = operation.getValues(); final String fieldName = operation.getFieldName(); - if (!documentContainsField(document, fieldName)) { - return C_FALSE; - } else if (expectedValues.isEmpty()) { + + if (expectedValues.isEmpty()) { return C_FALSE; + } else if (!documentContainsField(document, fieldName) || getValue(document, fieldName) == null) { + return (expectedValues.size() == 1 && expectedValues.get(0) == null) ? TRUE_C : C_FALSE; + } else if (!(getValue(document, fieldName) instanceof List)) { + return (expectedValues.size() == 1) ? computeHeuristicComparisonNullableValues(expectedValues.get(0), getValue(document, fieldName), SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO) : C_FALSE; } else { - Object actualValues = getValue(document, fieldName); - if (actualValues == null || !(actualValues instanceof List)) { + List actualValues = (List) getValue(document, fieldName); + if (actualValues.isEmpty()) { return C_FALSE; } else { - List actualValuesList = (List) actualValues; - if (actualValuesList.isEmpty()) { - return C_FALSE; - } else { - Truthness res = buildAndAggregationTruthness(actualValuesList - .stream() - .map(actualValuesListElement -> - computeHeuristic(actualValuesListElement, expectedValues)) - .toArray(Truthness[]::new)); - return buildSafeScaledTruthness(res); - } + Truthness res = buildAndAggregationTruthness(expectedValues + .stream() + .map(expectedValue -> + computeHeuristic(expectedValue, actualValues)) + .toArray(Truthness[]::new)); + return buildSafeScaledTruthness(res); } } } diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 9d1316dc86..88ac8388de 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -1660,7 +1660,6 @@ public void testFieldsHoldingASubDocument() { */ @Test - @Disabled("$all is quantified over the elements of the document instead of the expected values") public void testAllQuantifiesOverTheExpectedValues() { /* $all holds when every expected value is present in the array, and is @@ -1750,7 +1749,6 @@ public void testFieldsAreMatchedByAnElementOfTheArrayTheyHold() { } @Test - @Disabled("$all on a field that does not hold an array is reported as false") public void testAllOnAScalarField() { // mongo: a scalar is matched by a $all listing only values equal to it Document doc = new Document().append("tag", "a"); From 65fbb12402c7d5fc1650799617e31e081a0093ee Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Tue, 8 Sep 2026 12:41:08 -0300 Subject: [PATCH 03/53] Enhance `$all` heuristic evaluation to handle null and scalar field edge cases; add related test cases. --- .../mongo/MongoHeuristicsCalculator.java | 46 +++++++++++++------ .../mongo/MongoHeuristicsCalculatorTest.java | 38 +++++++++++++++ 2 files changed, 70 insertions(+), 14 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java index 4ede4d403a..113fd85c43 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java @@ -535,28 +535,46 @@ private Truthness computeHeuristic(NotInOperation operation, Object document) private Truthness computeHeuristic(AllOperation operation, Object document) { requireNonNullQueryAndDocument(operation, document); - List expectedValues = operation.getValues(); + final List expectedValues = operation.getValues(); final String fieldName = operation.getFieldName(); if (expectedValues.isEmpty()) { return C_FALSE; - } else if (!documentContainsField(document, fieldName) || getValue(document, fieldName) == null) { - return (expectedValues.size() == 1 && expectedValues.get(0) == null) ? TRUE_C : C_FALSE; - } else if (!(getValue(document, fieldName) instanceof List)) { - return (expectedValues.size() == 1) ? computeHeuristicComparisonNullableValues(expectedValues.get(0), getValue(document, fieldName), SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO) : C_FALSE; - } else { - List actualValues = (List) getValue(document, fieldName); - if (actualValues.isEmpty()) { + } + + if (!documentContainsField(document, fieldName)) { + return isSingleNullExpectedValue(expectedValues) ? TRUE_C : C_FALSE; + } + + final Object actualFieldValue = getValue(document, fieldName); + if (actualFieldValue == null) { + return isSingleNullExpectedValue(expectedValues) ? TRUE_C : C_FALSE; + } + + if (!(actualFieldValue instanceof List)) { + if (expectedValues.size() != 1) { return C_FALSE; } else { - Truthness res = buildAndAggregationTruthness(expectedValues - .stream() - .map(expectedValue -> - computeHeuristic(expectedValue, actualValues)) - .toArray(Truthness[]::new)); - return buildSafeScaledTruthness(res); + return computeHeuristicComparisonNullableValues( + expectedValues.get(0), + actualFieldValue, + SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO); } } + + List actualValues = (List) actualFieldValue; + if (actualValues.isEmpty()) { + return C_FALSE; + } + + Truthness res = buildAndAggregationTruthness(expectedValues.stream() + .map(expectedValue -> computeHeuristic(expectedValue, actualValues)) + .toArray(Truthness[]::new)); + return buildSafeScaledTruthness(res); + } + + private static boolean isSingleNullExpectedValue(List expectedValues) { + return expectedValues.size() == 1 && expectedValues.get(0) == null; } private static Truthness buildSafeScaledTruthness(Truthness truthness) { diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 88ac8388de..48869a7215 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -250,6 +250,24 @@ public void testAllMissingField() { assertTrue(calculator.computeHeuristicDocument(convertToDocument(allQuery), docUndefined).isFalse()); } + @Test + public void testAllMissingFieldWithSingleNullExpectedValue() { + Document docUndefined = new Document(); + Bson allQuery = Filters.all("employees", Collections.singletonList(null)); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + assertTrue(calculator.computeHeuristicDocument(convertToDocument(allQuery), docUndefined).isTrue()); + } + + @Test + public void testAllNullFieldWithSingleNullExpectedValue() { + Document docNull = new Document().append("employees", null); + Bson allQuery = Filters.all("employees", Collections.singletonList(null)); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + assertTrue(calculator.computeHeuristicDocument(convertToDocument(allQuery), docNull).isTrue()); + } + @Test public void testAllExpectedListIsEmpty() { Document document = new Document().append("employees", Arrays.asList("Bob", "Alice")); @@ -274,6 +292,26 @@ public void testAllBothActualAndExpectedListsAreEmpty() { assertTrue(calculator.computeHeuristicDocument(convertToDocument(allQuery), document).isFalse()); } + @Test + public void testAllOnScalarFieldWithSingleDifferentExpectedValue() { + Document document = new Document().append("tag", "a"); + Bson allQuery = Filters.all("tag", Collections.singletonList("b")); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + assertTrue(calculator.computeHeuristicDocument(convertToDocument(allQuery), document).isFalse()); + } + + @Test + public void testAllListFieldWithNullExpectedValue() { + Document document = new Document().append("employees", Arrays.asList("Bob", null, "Alice")); + Bson matchingAllQuery = Filters.all("employees", Collections.singletonList(null)); + Bson nonMatchingAllQuery = Filters.all("employees", Arrays.asList(null, "Carol")); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + assertTrue(calculator.computeHeuristicDocument(convertToDocument(matchingAllQuery), document).isTrue()); + assertTrue(calculator.computeHeuristicDocument(convertToDocument(nonMatchingAllQuery), document).isFalse()); + } + @Test public void testSize() { From 82c0e7c897415a7f90e3dfed5fb04a3b06dd187c Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Tue, 8 Sep 2026 14:57:31 -0300 Subject: [PATCH 04/53] Add comprehensive bitmask support for MongoDB queries and operations --- .../mongo/selectors/BitsAllClearSelector.java | 5 +- .../mongo/selectors/BitsAllSetSelector.java | 5 +- .../mongo/selectors/BitsAnyClearSelector.java | 5 +- .../mongo/selectors/BitsAnySetSelector.java | 7 +- .../controller/mongo/utils/BitmaskUtils.java | 59 ++++++++++++++ .../mongo/MongoHeuristicsCalculatorTest.java | 4 +- .../controller/mongo/QueryParserTest.java | 80 +++++++++++++++++-- .../mongo/utils/BitmaskUtilsTest.java | 66 +++++++++++++++ 8 files changed, 218 insertions(+), 13 deletions(-) create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BitmaskUtils.java create mode 100644 client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/BitmaskUtilsTest.java diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/BitsAllClearSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/BitsAllClearSelector.java index a5e5250feb..5f8b48da71 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/BitsAllClearSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/BitsAllClearSelector.java @@ -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 } } @@ -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 diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/BitsAllSetSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/BitsAllSetSelector.java index 503090f616..e09327275b 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/BitsAllSetSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/BitsAllSetSelector.java @@ -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 } } @@ -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 diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/BitsAnyClearSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/BitsAnyClearSelector.java index 3271545f57..c22f8d5a72 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/BitsAnyClearSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/BitsAnyClearSelector.java @@ -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 } } @@ -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 diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/BitsAnySetSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/BitsAnySetSelector.java index 10adb5186c..53421b1311 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/BitsAnySetSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/BitsAnySetSelector.java @@ -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 } } @@ -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; diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BitmaskUtils.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BitmaskUtils.java new file mode 100644 index 0000000000..6141adba54 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BitmaskUtils.java @@ -0,0 +1,59 @@ +package org.evomaster.client.java.controller.mongo.utils; + +import java.util.List; +import java.util.OptionalLong; + +public abstract class BitmaskUtils { + + /** + * Converts the given value into a bitmask representation if possible. + * The input value can be of the following types: + * - A {@code Number}, which is directly converted to its {@code long} value. + * - A {@code List} of numbers where each number represents a bit position. + * These positions are used to generate a bitmask by setting the corresponding bits. + * - A {@code byte[]} where the bytes represent the binary value of the bitmask. + * Only the least significant 64 bits are considered; higher-order bits must be zero. + * + * @param value the input value to convert. Must be a {@code Number}, a {@code List} of numbers, or a {@code byte[]} + * that satisfies the described conditions. + * @return an {@code OptionalLong} containing the resulting bitmask if the input value can be converted successfully; + * otherwise, an empty {@code OptionalLong}. + */ + public static OptionalLong toBitMaskValue(Object value) { + // value can be a number + if (value instanceof Number) { + return OptionalLong.of(((Number) value).longValue()); + } + // value can be a list of positions + if (value instanceof List) { + long mask = 0L; + for (Object p : (List) value) { + if (p instanceof Number) { + long pos = ((Number) p).longValue(); + if (pos >= Long.SIZE) { + return OptionalLong.empty(); // position 64+ — no fit + } + mask |= 1L << pos; // pos 63 -> Long.MIN_VALUE, fine + } else { + return OptionalLong.empty(); // not a number + } + } + return OptionalLong.of(mask); + } + // value can be a byte array + if (value instanceof byte[]) { + byte[] bytes = (byte[]) value; + for (int i = Long.BYTES; i < bytes.length; i++) { + if (bytes[i] != 0) { + return OptionalLong.empty(); // significant bits above 63 + } + } + long mask = 0L; + for (int i = 0; i < Math.min(bytes.length, Long.BYTES); i++) { + mask |= (bytes[i] & 0xFFL) << (8 * i); // byte 0 holds positions 0-7 + } + return OptionalLong.of(mask); + } + return OptionalLong.empty(); + } +} diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 48869a7215..46f1043721 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -1609,7 +1609,6 @@ public void testTypeWithAStringAlias() { } @Test - @Disabled("a bitmask given as an Integer is not parsed, so the calculator throws a NullPointerException") public void testBitsWithAnIntegerBitmask() { /* mongo: matches, any integer bitmask is accepted. It arrives as an Integer @@ -1813,7 +1812,7 @@ public void testInMatchingAnArrayFieldAsAWhole() { } @Test - @Disabled("a bitwise operator truncates a non-integral number instead of not matching it") + //@Disabled("a bitwise operator truncates a non-integral number instead of not matching it") public void testBitsDoesNotMatchANonIntegralNumber() { // mongo: 3.0 is an integer and is matched, 3.5 is not truncated to 3 MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); @@ -1917,7 +1916,6 @@ public void testTypeWithAListOfAliases() { } @Test - @Disabled("$bits with a list of bit positions is not parsed, so the calculator throws a NullPointerException") public void testBitsWithBitPositions() { // mongo: matches, bits 0 and 2 are set in 5. The bitmask can be given as positions Document query = new Document().append("a", diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java index 66623599f8..5950cf21b4 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java @@ -6,9 +6,13 @@ import org.bson.codecs.DecoderContext; import org.bson.codecs.DocumentCodec; import org.bson.conversions.Bson; +import org.bson.types.Decimal128; import org.evomaster.client.java.controller.mongo.operations.*; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -555,7 +559,7 @@ void testParseMod() { } @Test - void testParseBitsAllClear() { + void testParseBitsAllClearLong() { Document query = new Document( "flags", new Document("$bitsAllClear", 5L) @@ -568,10 +572,49 @@ void testParseBitsAllClear() { } @Test - void testParseBitsAllSet() { + void testParseBitsAllClearInteger() { Document query = new Document( "flags", - new Document("$bitsAllSet", 5L) + new Document("$bitsAllClear", 5) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof BitsAllClearOperation); + BitsAllClearOperation bitsAllClear = (BitsAllClearOperation) operation; + assertEquals("flags", bitsAllClear.getFieldName()); + assertEquals(5L, bitsAllClear.getBitmask()); + } + + @Test + void testParseBitsAllClearDouble() { + Document query = new Document( + "flags", + new Document("$bitsAllClear", 5.0d) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof BitsAllClearOperation); + BitsAllClearOperation bitsAllClear = (BitsAllClearOperation) operation; + assertEquals("flags", bitsAllClear.getFieldName()); + assertEquals(5L, bitsAllClear.getBitmask()); + } + + @Test + void testParseBitsAllClearBigDecimal() { + Document query = new Document( + "flags", + new Document("$bitsAllClear", new Decimal128(5)) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof BitsAllClearOperation); + BitsAllClearOperation bitsAllClear = (BitsAllClearOperation) operation; + assertEquals("flags", bitsAllClear.getFieldName()); + assertEquals(5L, bitsAllClear.getBitmask()); + } + + @Test + void testParseBitsAllSetInteger() { + Document query = new Document( + "flags", + new Document("$bitsAllSet", 5) ); QueryOperation operation = parser.parse(query); assertTrue(operation instanceof BitsAllSetOperation); @@ -581,7 +624,7 @@ void testParseBitsAllSet() { } @Test - void testParseBitsAnyClear() { + void testParseBitsAnyClearLong() { Document query = new Document( "flags", new Document("$bitsAnyClear", 5L) @@ -594,7 +637,20 @@ void testParseBitsAnyClear() { } @Test - void testParseBitsAnySet() { + void testParseBitsAnyClearInteger() { + Document query = new Document( + "flags", + new Document("$bitsAnyClear", 5) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof BitsAnyClearOperation); + BitsAnyClearOperation bitsAnyClear = (BitsAnyClearOperation) operation; + assertEquals("flags", bitsAnyClear.getFieldName()); + assertEquals(5L, bitsAnyClear.getBitmask()); + } + + @Test + void testParseBitsAnySetLong() { Document query = new Document( "flags", new Document("$bitsAnySet", 5L) @@ -606,6 +662,19 @@ void testParseBitsAnySet() { assertEquals(5L, bitsAnySet.getBitmask()); } + @Test + void testParseBitsAnySetBigDecimal128() { + Document query = new Document( + "flags", + new Document("$bitsAnySet", new Decimal128(5)) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof BitsAnySetOperation); + BitsAnySetOperation bitsAnySet = (BitsAnySetOperation) operation; + assertEquals("flags", bitsAnySet.getFieldName()); + assertEquals(5L, bitsAnySet.getBitmask()); + } + @Test void testParseGreaterThanEquals() { Document query = new Document( @@ -1616,7 +1685,6 @@ void testParseInvalidBitwiseValues() { assertAll( () -> assertInvalidQuery( new Document("flags", new Document("$bitsAllClear", "5"))), - () -> assertInvalidQuery(new Document("flags", new Document("$bitsAllSet", 5))), () -> assertInvalidQuery( new Document("flags", new Document("$bitsAnyClear", true))), () -> assertInvalidQuery( diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/BitmaskUtilsTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/BitmaskUtilsTest.java new file mode 100644 index 0000000000..46b914688f --- /dev/null +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/BitmaskUtilsTest.java @@ -0,0 +1,66 @@ +package org.evomaster.client.java.controller.mongo.utils; + +import org.bson.types.Decimal128; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collections; +import java.util.OptionalLong; + +import static org.junit.jupiter.api.Assertions.*; + +class BitmaskUtilsTest { + + @Test + void testToBitMaskValueWithNumber() { + assertAll( + () -> assertEquals(OptionalLong.of(5L), BitmaskUtils.toBitMaskValue(5)), + () -> assertEquals(OptionalLong.of(7L), BitmaskUtils.toBitMaskValue(7.9d)), + () -> assertEquals(OptionalLong.of(9L), BitmaskUtils.toBitMaskValue(9L)), + () -> assertEquals(OptionalLong.of(11L), BitmaskUtils.toBitMaskValue(new Decimal128(new BigDecimal("11")))) + ); + } + + @Test + void testToBitMaskValueWithListPositions() { + assertAll( + () -> assertEquals(OptionalLong.of(11L), BitmaskUtils.toBitMaskValue(Arrays.asList(0, 1, 3))), + () -> assertEquals(OptionalLong.of(0L), BitmaskUtils.toBitMaskValue(Collections.emptyList())), + () -> assertEquals(OptionalLong.of(2L), BitmaskUtils.toBitMaskValue(Arrays.asList(1, 1))), + () -> assertEquals(OptionalLong.of(Long.MIN_VALUE), BitmaskUtils.toBitMaskValue(Collections.singletonList(63L))) + ); + } + + @Test + void testToBitMaskValueWithInvalidListPositions() { + assertAll( + () -> assertEquals(OptionalLong.empty(), BitmaskUtils.toBitMaskValue(Arrays.asList(1, "x"))), + () -> assertEquals(OptionalLong.empty(), BitmaskUtils.toBitMaskValue(Collections.singletonList(64L))) + ); + } + + @Test + void testToBitMaskValueWithByteArray() { + assertAll( + () -> assertEquals(OptionalLong.of(5L), BitmaskUtils.toBitMaskValue(new byte[]{0x05})), + () -> assertEquals(OptionalLong.of(257L), BitmaskUtils.toBitMaskValue(new byte[]{0x01, 0x01})), + () -> assertEquals(OptionalLong.of(1L), BitmaskUtils.toBitMaskValue(new byte[]{0x01, 0, 0, 0, 0, 0, 0, 0, 0})) + ); + } + + @Test + void testToBitMaskValueWithInvalidByteArray() { + assertEquals(OptionalLong.empty(), BitmaskUtils.toBitMaskValue(new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 1})); + } + + @Test + void testToBitMaskValueWithUnsupportedType() { + assertAll( + () -> assertEquals(OptionalLong.empty(), BitmaskUtils.toBitMaskValue("not-a-bitmask")), + () -> assertEquals(OptionalLong.empty(), BitmaskUtils.toBitMaskValue(new Object())), + () -> assertEquals(OptionalLong.empty(), BitmaskUtils.toBitMaskValue(null)) + ); + } + +} From a07a0e31a3439d70b39b75550a168ada7220b4de Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Tue, 8 Sep 2026 15:08:48 -0300 Subject: [PATCH 05/53] Add `MongoUtils` utility for integral number handling; integrate with bitmask operations --- .../mongo/MongoHeuristicsCalculator.java | 11 ++- .../controller/mongo/utils/MongoUtils.java | 60 ++++++++++++++++ .../mongo/MongoHeuristicsCalculatorTest.java | 1 - .../mongo/utils/MongoUtilsTest.java | 69 +++++++++++++++++++ 4 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/MongoUtils.java create mode 100644 client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/MongoUtilsTest.java diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java index 113fd85c43..b571373c44 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java @@ -5,6 +5,7 @@ import org.evomaster.client.java.controller.mongo.geometry.GeoJsonUtils; import org.evomaster.client.java.controller.mongo.operations.*; import org.evomaster.client.java.controller.mongo.utils.BsonHelper; +import org.evomaster.client.java.controller.mongo.utils.MongoUtils; import org.evomaster.client.java.distance.heuristics.Truthness; import org.evomaster.client.java.instrumentation.coverage.methodreplacement.RegexDistanceUtils; import org.evomaster.client.java.instrumentation.shared.StringSpecialization; @@ -18,6 +19,8 @@ import static org.evomaster.client.java.distance.heuristics.TruthnessUtils.*; import static org.evomaster.client.java.sql.heuristic.ConversionHelper.convertToInstant; +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -715,8 +718,13 @@ private Truthness computeHeuristic(BitsOperation operation, Object document) { return C_FALSE; } + final OptionalLong integralValue = MongoUtils.getIntegralLongValue((Number) actualValue); + if (!integralValue.isPresent()) { + return C_FALSE; + } + final long bitmask = operation.getBitmask(); - final long maskedValue = ((Number) actualValue).longValue() & bitmask; + final long maskedValue = integralValue.getAsLong() & bitmask; final int numberOfSetBitsInMaskedValue = Long.bitCount(maskedValue); final int numberOfBitsInMask = Long.bitCount(bitmask); if (operation instanceof BitsAllClearOperation) { @@ -736,7 +744,6 @@ private Truthness computeHeuristic(BitsOperation operation, Object document) { } } - private Truthness computeHeuristic(NotOperation operation, Object document) { requireNonNullQueryAndDocument(operation, document); diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/MongoUtils.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/MongoUtils.java new file mode 100644 index 0000000000..4111dad1c5 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/MongoUtils.java @@ -0,0 +1,60 @@ +package org.evomaster.client.java.controller.mongo.utils; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Objects; +import java.util.OptionalLong; + +public abstract class MongoUtils { + + + /** + * Converts the provided {@code Number} into a long value if it represents an integral number + * within the range of a {@code long}. Supports various {@code Number} types including + * {@code Byte}, {@code Short}, {@code Integer}, {@code Long}, {@code BigInteger}, and + * {@code BigDecimal}. + * + * If the provided number is of type {@code BigInteger} or {@code BigDecimal} and falls outside + * the range of {@code long}, or if it cannot be expressed as an exact integral value, + * the method will return an empty {@code OptionalLong}. + * + * @param value the {@code Number} to convert. Must not be {@code null}. + * @return an {@code OptionalLong} containing the long value if the input represents a valid integral + * value within the range of {@code long}, or an empty {@code OptionalLong} otherwise. + * @throws NullPointerException if the provided {@code value} is {@code null}. + */ + public static OptionalLong getIntegralLongValue(Number value) { + Objects.requireNonNull(value); + + if (value instanceof Byte || value instanceof Short || value instanceof Integer || value instanceof Long) { + return OptionalLong.of(value.longValue()); + } + + if (value instanceof BigInteger) { + BigInteger bigInteger = (BigInteger) value; + if (bigInteger.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0 + || bigInteger.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0) { + return OptionalLong.empty(); + } + return OptionalLong.of(bigInteger.longValue()); + } + + if (value instanceof BigDecimal) { + try { + return OptionalLong.of(((BigDecimal) value).longValueExact()); + } catch (ArithmeticException e) { + return OptionalLong.empty(); + } + } + + double doubleValue = value.doubleValue(); + if (!Double.isFinite(doubleValue) + || doubleValue < Long.MIN_VALUE + || doubleValue > Long.MAX_VALUE + || doubleValue != Math.rint(doubleValue)) { + return OptionalLong.empty(); + } + + return OptionalLong.of((long) doubleValue); + } +} diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 46f1043721..1bc27861fc 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -1812,7 +1812,6 @@ public void testInMatchingAnArrayFieldAsAWhole() { } @Test - //@Disabled("a bitwise operator truncates a non-integral number instead of not matching it") public void testBitsDoesNotMatchANonIntegralNumber() { // mongo: 3.0 is an integer and is matched, 3.5 is not truncated to 3 MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/MongoUtilsTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/MongoUtilsTest.java new file mode 100644 index 0000000000..276dcef546 --- /dev/null +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/MongoUtilsTest.java @@ -0,0 +1,69 @@ +package org.evomaster.client.java.controller.mongo.utils; + +import org.bson.types.Decimal128; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.OptionalLong; + +import static org.junit.jupiter.api.Assertions.*; + +class MongoUtilsTest { + + @Test + void testGetIntegralLongValueWithPrimitiveIntegralTypes() { + assertAll( + () -> assertEquals(OptionalLong.of(1L), MongoUtils.getIntegralLongValue((byte) 1)), + () -> assertEquals(OptionalLong.of(2L), MongoUtils.getIntegralLongValue((short) 2)), + () -> assertEquals(OptionalLong.of(3L), MongoUtils.getIntegralLongValue(3)), + () -> assertEquals(OptionalLong.of(4L), MongoUtils.getIntegralLongValue(4L)) + ); + } + + @Test + void testGetIntegralLongValueWithBigInteger() { + assertAll( + () -> assertEquals(OptionalLong.of(Long.MIN_VALUE), MongoUtils.getIntegralLongValue(BigInteger.valueOf(Long.MIN_VALUE))), + () -> assertEquals(OptionalLong.of(Long.MAX_VALUE), MongoUtils.getIntegralLongValue(BigInteger.valueOf(Long.MAX_VALUE))), + () -> assertEquals(OptionalLong.of(42L), MongoUtils.getIntegralLongValue(new BigInteger("42"))), + () -> assertEquals(OptionalLong.empty(), MongoUtils.getIntegralLongValue(BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE))), + () -> assertEquals(OptionalLong.empty(), MongoUtils.getIntegralLongValue(BigInteger.valueOf(Long.MIN_VALUE).subtract(BigInteger.ONE))) + ); + } + + @Test + void testGetIntegralLongValueWithBigDecimal() { + assertAll( + () -> assertEquals(OptionalLong.of(12L), MongoUtils.getIntegralLongValue(new BigDecimal("12"))), + () -> assertEquals(OptionalLong.empty(), MongoUtils.getIntegralLongValue(new BigDecimal("12.5"))), + () -> assertEquals(OptionalLong.empty(), MongoUtils.getIntegralLongValue(new BigDecimal("9223372036854775808"))) + ); + } + + @Test + void testGetIntegralLongValueWithFloatingPointNumbers() { + assertAll( + () -> assertEquals(OptionalLong.of(5L), MongoUtils.getIntegralLongValue(5.0d)), + () -> assertEquals(OptionalLong.of(6L), MongoUtils.getIntegralLongValue(6.0f)), + () -> assertEquals(OptionalLong.empty(), MongoUtils.getIntegralLongValue(5.5d)), + () -> assertEquals(OptionalLong.empty(), MongoUtils.getIntegralLongValue(Double.NaN)), + () -> assertEquals(OptionalLong.empty(), MongoUtils.getIntegralLongValue(Double.POSITIVE_INFINITY)), + () -> assertEquals(OptionalLong.empty(), MongoUtils.getIntegralLongValue(Double.NEGATIVE_INFINITY)) + ); + } + + @Test + void testGetIntegralLongValueWithDecimal128() { + assertAll( + () -> assertEquals(OptionalLong.of(9L), MongoUtils.getIntegralLongValue(new Decimal128(new BigDecimal("9")))), + () -> assertEquals(OptionalLong.empty(), MongoUtils.getIntegralLongValue(new Decimal128(new BigDecimal("9.1")))) + ); + } + + @Test + void testGetIntegralLongValueRejectsNull() { + assertThrows(NullPointerException.class, () -> MongoUtils.getIntegralLongValue(null)); + } + +} From c435d3be8e00c48c51e79c4d9b953da567a709cd Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Tue, 8 Sep 2026 15:30:24 -0300 Subject: [PATCH 06/53] Refactor number comparison logic in Mongo heuristics to handle NaN cases. --- .../mongo/MongoHeuristicsCalculator.java | 45 ++++++++++++++++--- .../mongo/MongoHeuristicsCalculatorTest.java | 1 - 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java index b571373c44..7eca37a826 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java @@ -8,10 +8,6 @@ import org.evomaster.client.java.controller.mongo.utils.MongoUtils; import org.evomaster.client.java.distance.heuristics.Truthness; import org.evomaster.client.java.instrumentation.coverage.methodreplacement.RegexDistanceUtils; -import org.evomaster.client.java.instrumentation.shared.StringSpecialization; -import org.evomaster.client.java.instrumentation.shared.StringSpecializationInfo; -import org.evomaster.client.java.instrumentation.shared.TaintType; -import org.evomaster.client.java.instrumentation.staticstate.ExecutionTracer; import org.evomaster.client.java.sql.heuristic.SqlExpressionEvaluator; import org.evomaster.client.java.sql.internal.TaintHandler; @@ -19,8 +15,6 @@ import static org.evomaster.client.java.distance.heuristics.TruthnessUtils.*; import static org.evomaster.client.java.sql.heuristic.ConversionHelper.convertToInstant; -import java.math.BigDecimal; -import java.math.BigInteger; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -240,7 +234,9 @@ private Truthness computeHeuristicComparisonNonNullValues(Object actualValue, Ob final Truthness truthnessOfComparison; if (actualValue instanceof Number && expectedValue instanceof Number) { - truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForNumberComparison((Number) actualValue, (Number) expectedValue, comparisonOperatorType); + final Number actualNumber = (Number) actualValue; + final Number expectedNumber = (Number) expectedValue; + truthnessOfComparison = computeHeuristicComparisonNumberValues(actualNumber, expectedNumber, comparisonOperatorType); } else if (actualValue instanceof String && expectedValue instanceof String) { String actualString = (String) actualValue; @@ -284,6 +280,41 @@ private Truthness computeHeuristicComparisonNonNullValues(Object actualValue, Ob return truthnessOfComparison; } + private static Truthness computeHeuristicComparisonNumberValues(Number leftNumber, Number rightNumber, SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType) { + Objects.requireNonNull(leftNumber); + Objects.requireNonNull(rightNumber); + Objects.requireNonNull(comparisonOperatorType); + + double leftValueAsDouble = leftNumber.doubleValue(); + double rightValueAsDouble = rightNumber.doubleValue(); + + if (Double.isNaN(leftValueAsDouble) || Double.isNaN(rightValueAsDouble)) { + // handle case when NaN is involved in the comparison + switch (comparisonOperatorType) { + case EQUALS_TO: { + return (Double.isNaN(leftValueAsDouble) && Double.isNaN(rightValueAsDouble)) ? + TRUE_C : C_FALSE; + } + case NOT_EQUALS_TO: { + return (!Double.isNaN(leftValueAsDouble) || !Double.isNaN(rightValueAsDouble)) ? + TRUE_C : C_FALSE; + } + case GREATER_THAN: + case GREATER_THAN_EQUALS: + case MINOR_THAN: + case MINOR_THAN_EQUALS: { + return C_FALSE; + } + default: + throw new IllegalArgumentException("Unsupported comparison operator type: " + comparisonOperatorType); + } + } else { + // if both values are not NaN, we can use the standard comparison logic + final Truthness truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForNumberComparison(leftNumber, rightNumber, comparisonOperatorType); + return truthnessOfComparison; + } + } + private static int toIntValue(Boolean actualValue) { return actualValue ? 1 : 0; } diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 1bc27861fc..b6ea3887c8 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -1854,7 +1854,6 @@ public void testDottedFieldPath() { */ @Test - @Disabled("an ordering comparison involving NaN makes the calculator throw IllegalArgumentException") public void testOrderingComparisonWithNaN() { /* mongo: no match for either, NaN does not order against anything. From 42ee0b5da49492930610244947a5c46b28d9ad92 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Tue, 8 Sep 2026 16:40:35 -0300 Subject: [PATCH 07/53] Refactor heuristic calculation to support scalar-to-list comparisons; re-enable disabled tests and migrate geospatial logic to `MongoUtils`. --- .../mongo/MongoHeuristicsCalculator.java | 183 +++++++----------- .../controller/mongo/utils/MongoUtils.java | 99 ++++++++++ .../mongo/MongoHeuristicsCalculatorTest.java | 16 +- 3 files changed, 178 insertions(+), 120 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java index 7eca37a826..471e1a2c85 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java @@ -12,9 +12,12 @@ import org.evomaster.client.java.sql.internal.TaintHandler; import static org.evomaster.client.java.controller.mongo.utils.BsonHelper.*; +import static org.evomaster.client.java.controller.mongo.utils.MongoUtils.*; +import static org.evomaster.client.java.controller.mongo.utils.MongoUtils.GeoSpatialModel.SPHERICAL; import static org.evomaster.client.java.distance.heuristics.TruthnessUtils.*; import static org.evomaster.client.java.sql.heuristic.ConversionHelper.convertToInstant; + import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -205,7 +208,7 @@ private Truthness computeHeuristic(NearOperation operation, Object document) { GeoSpatialModel model = operation.hasLegacyCoordinates() ? GeoSpatialModel.PLANAR - : GeoSpatialModel.SPHERICAL; + : SPHERICAL; return computeHeuristic(operation, longitude, latitude, fieldValue, model); } @@ -361,10 +364,14 @@ private Truthness computeHeuristic(EqualsOperation operation, Object document actualValue = null; } - return computeHeuristicComparisonNullableValues( - expectedValue, - actualValue, - SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO); + if ((actualValue instanceof List) && !(expectedValue instanceof List)) { + return computeHeuristic(expectedValue, (List) actualValue); + } else { + return computeHeuristicComparisonNullableValues( + expectedValue, + actualValue, + SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO); + } } private Truthness computeHeuristicComparisonNullableValues(Object expectedValue, Object actualValue, SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType) { @@ -418,10 +425,14 @@ private Truthness computeHeuristic(NotEqualsOperation operation, Object docum } else { actualValue = null; } - return computeHeuristicComparisonNullableValues( - expectedValue, - actualValue, - SqlExpressionEvaluator.ComparisonOperatorType.NOT_EQUALS_TO); + if ((actualValue instanceof List) && !(expectedValue instanceof List)) { + return computeHeuristic(expectedValue, (List) actualValue).invert(); + } else { + return computeHeuristicComparisonNullableValues( + expectedValue, + actualValue, + SqlExpressionEvaluator.ComparisonOperatorType.NOT_EQUALS_TO); + } } @@ -534,9 +545,14 @@ private Truthness computeHeuristic(InOperation operation, Object document) { final Truthness res; if (actualValue instanceof List) { List actualValueList = (List) actualValue; - res = buildOrAggregationTruthness(actualValueList.stream() - .map(value -> computeHeuristic(value, expectedValueList)) - .toArray(Truthness[]::new)); + if (actualValueList.isEmpty()) { + res = C_FALSE; + } else { + Truthness orAggregation = buildOrAggregationTruthness(actualValueList.stream() + .map(value -> computeHeuristic(value, expectedValueList)) + .toArray(Truthness[]::new)); + res = buildSafeScaledTruthness(orAggregation); + } } else { res = computeHeuristic(actualValue, expectedValueList); } @@ -545,11 +561,14 @@ private Truthness computeHeuristic(InOperation operation, Object document) { private Truthness computeHeuristic(Object actualValue, List expectedValueList) { Objects.requireNonNull(expectedValueList); - - Truthness res = buildOrAggregationTruthness(expectedValueList.stream() - .map(expectedValue -> computeHeuristicComparisonNullableValues(expectedValue, actualValue, SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO)) - .toArray(Truthness[]::new)); - return res; + if (expectedValueList.isEmpty()) { + return C_FALSE; + } else { + Truthness res = buildOrAggregationTruthness(expectedValueList.stream() + .map(expectedValue -> computeHeuristicComparisonNullableValues(expectedValue, actualValue, SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO)) + .toArray(Truthness[]::new)); + return buildSafeScaledTruthness(res); + } } private Truthness computeHeuristic(NotInOperation operation, Object document) { @@ -749,7 +768,7 @@ private Truthness computeHeuristic(BitsOperation operation, Object document) { return C_FALSE; } - final OptionalLong integralValue = MongoUtils.getIntegralLongValue((Number) actualValue); + final OptionalLong integralValue = getIntegralLongValue((Number) actualValue); if (!integralValue.isPresent()) { return C_FALSE; } @@ -826,24 +845,10 @@ private Truthness computeHeuristic(NearSphereOperation operation, Object documen final double longitude = operation.getLongitude(); final double latitude = operation.getLatitude(); - return computeHeuristic(operation, longitude, latitude, fieldValue, GeoSpatialModel.SPHERICAL); + return computeHeuristic(operation, longitude, latitude, fieldValue, SPHERICAL); } - /** - * Enumeration representing the geospatial model used for distance calculations. - * PLANAR: Uses Euclidean distance for flat surfaces. - * SPHERICAL: Uses Haversine distance for spherical surfaces (e.g., Earth) - */ - private enum GeoSpatialModel { - /** - * PLANAR: Uses Euclidean distance for flat surfaces. - */ - PLANAR, - /** - * SPHERICAL: Uses Haversine distance for spherical surfaces (e.g., Earth) - */ - SPHERICAL - } + private static Truthness computeHeuristic(AbstractProximityOperation abstractProximityOperation, double longitude, @@ -852,8 +857,8 @@ private static Truthness computeHeuristic(AbstractProximityOperation abstractPro GeoSpatialModel geoSpatialModel) { Objects.requireNonNull(abstractProximityOperation); - double x1 = geoSpatialModel == GeoSpatialModel.SPHERICAL ? Math.toRadians(longitude) : longitude; - double y1 = geoSpatialModel == GeoSpatialModel.SPHERICAL ? Math.toRadians(latitude) : latitude; + double x1 = geoSpatialModel == SPHERICAL ? Math.toRadians(longitude) : longitude; + double y1 = geoSpatialModel == SPHERICAL ? Math.toRadians(latitude) : latitude; double x2; double y2; @@ -865,26 +870,16 @@ private static Truthness computeHeuristic(AbstractProximityOperation abstractPro if (isBsonDocument(fieldValue) && GeoJsonUtils.isGeoJsonPoint(fieldValue)) { GeoJsonPoint geoJsonPoint = GeoJsonUtils.toGeoJsonPoint(fieldValue); - x2 = geoSpatialModel == GeoSpatialModel.SPHERICAL + x2 = geoSpatialModel == SPHERICAL ? Math.toRadians(geoJsonPoint.getLongitude()) : geoJsonPoint.getLongitude(); - y2 = geoSpatialModel == GeoSpatialModel.SPHERICAL + y2 = geoSpatialModel == SPHERICAL ? Math.toRadians(geoJsonPoint.getLatitude()) : geoJsonPoint.getLatitude(); } else { return C_FALSE; } - double distanceBetweenPoints; - switch (geoSpatialModel) { - case PLANAR: - distanceBetweenPoints = euclideanDistance(x1, y1, x2, y2); - break; - case SPHERICAL: - distanceBetweenPoints = haversineDistance(x1, y1, x2, y2); - break; - default: - throw new IllegalArgumentException("Unsupported GeoSpatialModel: " + geoSpatialModel); - } + double distanceBetweenPoints = getDistanceBetweenPoints(x1, y1, x2, y2, geoSpatialModel); double max = abstractProximityOperation.hasMaxDistance() ? abstractProximityOperation.getMaxDistance() : Double.MAX_VALUE; @@ -903,77 +898,11 @@ private static Truthness computeHeuristic(AbstractProximityOperation abstractPro : getEqualityTruthness(distanceBetweenPoints, min); } - - /** - * Calculates the Haversine distance between two geographical points specified - * in radians. The Haversine formula determines the great-circle distance between - * two points on a sphere given their latitudes and longitudes. - * - * @param x1 the longitude of the first point in radians - * @param y1 the latitude of the first point in radians - * @param x2 the longitude of the second point in radians - * @param y2 the latitude of the second point in radians - * @return the Haversine distance between the two points in meters - */ - private static double haversineDistance( - double x1, - double y1, - double x2, - double y2) { - - // Earth's radius in meters - double radius = 6371000.0; - - double dLat = y2 - y1; - double dLon = x2 - x1; - - double a = Math.pow(Math.sin(dLat / 2), 2) - + Math.cos(y1) * Math.cos(y2) - * Math.pow(Math.sin(dLon / 2), 2); - - double c = 2 * Math.atan2( - Math.sqrt(a), - Math.sqrt(1 - a)); - - return radius * c; - } - - /** - * Calculates the Euclidean distance between two points in a 2D Cartesian coordinate system. - * The Euclidean distance is the straight-line distance between two points in Euclidean space. - * - * @param x1 the x-coordinate of the first point - * @param y1 the y-coordinate of the first point - * @param x2 the x-coordinate of the second point - * @param y2 the y-coordinate of the second point - * @return the Euclidean distance between the two points - */ - private static double euclideanDistance( - double x1, - double y1, - double x2, - double y2) { - return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); - } - private Truthness calculateTruthnessForListComparison(List actualList, List expectedList, SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType) { Objects.requireNonNull(actualList); Objects.requireNonNull(expectedList); - final Truthness truthness; - if (actualList.size() != expectedList.size()) { - truthness = C_FALSE; - } else { - Truthness[] arrayOfTruthnesses = new Truthness[actualList.size()]; - for (int i = 0; i < actualList.size(); i++) { - arrayOfTruthnesses[i] = computeHeuristicComparisonNullableValues( - actualList.get(i), - expectedList.get(i), - SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO); - } - Truthness unscaledTruthness = buildAndAggregationTruthness(arrayOfTruthnesses); - truthness = buildSafeScaledTruthness(unscaledTruthness); - } + final Truthness truthness = computeHeuristicEquality(actualList, expectedList); switch (comparisonOperatorType) { case EQUALS_TO: return truthness; @@ -984,5 +913,27 @@ private Truthness calculateTruthnessForListComparison(List actualList, List actualList, List expectedList) { + + if (actualList.size() != expectedList.size()) { + return C_FALSE; + } + + if (actualList.isEmpty() && expectedList.isEmpty()) { + return TRUE_C; + } + + Truthness[] arrayOfTruthnesses = new Truthness[actualList.size()]; + for (int i = 0; i < actualList.size(); i++) { + arrayOfTruthnesses[i] = computeHeuristicComparisonNullableValues( + actualList.get(i), + expectedList.get(i), + SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO); + } + Truthness unscaledTruthness = buildAndAggregationTruthness(arrayOfTruthnesses); + final Truthness truthness = buildSafeScaledTruthness(unscaledTruthness); + return truthness; + } + } diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/MongoUtils.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/MongoUtils.java index 4111dad1c5..9b496b3306 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/MongoUtils.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/MongoUtils.java @@ -7,6 +7,105 @@ public abstract class MongoUtils { + /** + * Enumeration representing the geospatial model used for distance calculations. + * PLANAR: Uses Euclidean distance for flat surfaces. + * SPHERICAL: Uses Haversine distance for spherical surfaces (e.g., Earth) + */ + public enum GeoSpatialModel { + /** + * PLANAR: Uses Euclidean distance for flat surfaces. + */ + PLANAR, + /** + * SPHERICAL: Uses Haversine distance for spherical surfaces (e.g., Earth) + */ + SPHERICAL + } + + /** + * Calculates the Euclidean distance between two points in a 2D Cartesian coordinate system. + * The Euclidean distance is the straight-line distance between two points in Euclidean space. + * + * @param x1 the x-coordinate of the first point + * @param y1 the y-coordinate of the first point + * @param x2 the x-coordinate of the second point + * @param y2 the y-coordinate of the second point + * @return the Euclidean distance between the two points + */ + private static double euclideanDistance( + double x1, + double y1, + double x2, + double y2) { + return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); + } + + /** + * Calculates the Haversine distance between two geographical points specified + * in radians. The Haversine formula determines the great-circle distance between + * two points on a sphere given their latitudes and longitudes. + * + * @param x1 the longitude of the first point in radians + * @param y1 the latitude of the first point in radians + * @param x2 the longitude of the second point in radians + * @param y2 the latitude of the second point in radians + * @return the Haversine distance between the two points in meters + */ + private static double haversineDistance( + double x1, + double y1, + double x2, + double y2) { + + // Earth's radius in meters + double radius = 6371000.0; + + double dLat = y2 - y1; + double dLon = x2 - x1; + + double a = Math.pow(Math.sin(dLat / 2), 2) + + Math.cos(y1) * Math.cos(y2) + * Math.pow(Math.sin(dLon / 2), 2); + + double c = 2 * Math.atan2( + Math.sqrt(a), + Math.sqrt(1 - a)); + + return radius * c; + } + + /** + * Calculates the distance between two points based on the specified geospatial model. + * This method supports both planar (Euclidean) and spherical (Haversine) distance calculations + * depending on the provided {@code GeoSpatialModel}. + * + * @param x1 the x-coordinate (or longitude in radians for spherical model) of the first point + * @param y1 the y-coordinate (or latitude in radians for spherical model) of the first point + * @param x2 the x-coordinate (or longitude in radians for spherical model) of the second point + * @param y2 the y-coordinate (or latitude in radians for spherical model) of the second point + * @param geoSpatialModel the geospatial model to apply for distance calculation; must be either + * {@code GeoSpatialModel.PLANAR} or {@code GeoSpatialModel.SPHERICAL} + * @return the calculated distance between the two points. For the planar model, this value + * represents the Euclidean distance. For the spherical model, this is the Haversine + * distance in meters. + * @throws IllegalArgumentException if an unsupported {@code GeoSpatialModel} is provided + */ + public static double getDistanceBetweenPoints(double x1, double y1, double x2, double y2, GeoSpatialModel geoSpatialModel) { + double distanceBetweenPoints; + switch (geoSpatialModel) { + case PLANAR: + distanceBetweenPoints = euclideanDistance(x1, y1, x2, y2); + break; + case SPHERICAL: + distanceBetweenPoints = haversineDistance(x1, y1, x2, y2); + break; + default: + throw new IllegalArgumentException("Unsupported GeoSpatialModel: " + geoSpatialModel); + } + return distanceBetweenPoints; + } + /** * Converts the provided {@code Number} into a long value if it represents an integral number diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index b6ea3887c8..7aadf62efd 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -31,6 +31,18 @@ public class MongoHeuristicsCalculatorTest { + @Test + public void testEqualsScalarToList() { + Document doc = new Document().append("tags", Arrays.asList("red","blue")); + Bson bsonTrue = Filters.eq("tags", "red"); + Bson bsonFalse = Filters.eq("tags", "green"); + Truthness distanceMatch = new MongoHeuristicsCalculator().computeHeuristicDocument(convertToDocument(bsonTrue), doc); + Truthness distanceNotMatch = new MongoHeuristicsCalculator().computeHeuristicDocument(convertToDocument(bsonFalse), doc); + assertTrue(distanceMatch.isTrue()); + assertTrue(distanceNotMatch.isFalse()); + } + + @Test public void testEquals() { Document doc = new Document().append("age", 10); @@ -1622,7 +1634,6 @@ public void testBitsWithAnIntegerBitmask() { } @Test - @Disabled("an empty list of values makes the calculator throw IllegalArgumentException") public void testInAndNotInWithAnEmptyListOfValues() { // mongo: {$in: []} matches nothing, {$nin: []} matches everything Document doc = new Document().append("tags", new ArrayList<>(Arrays.asList("a", "b"))); @@ -1635,7 +1646,6 @@ public void testInAndNotInWithAnEmptyListOfValues() { } @Test - @Disabled("an empty array in the document makes the calculator throw IllegalArgumentException") public void testInAndNotInAgainstAnEmptyArrayField() { // mongo: an empty array holds no value, so $in does not match and $nin does Document doc = new Document().append("tags", Collections.emptyList()); @@ -1648,7 +1658,6 @@ public void testInAndNotInAgainstAnEmptyArrayField() { } @Test - @Disabled("comparing two empty arrays makes the calculator throw IllegalArgumentException") public void testEqualsBetweenEmptyArrays() { /* mongo: matches, two empty arrays are equal. The two are of equal size, so the @@ -1886,7 +1895,6 @@ public void testOrderingComparisonBetweenAnObjectIdAndANumber() { } @Test - @Disabled("null held as an element of an array is not matched") public void testNullInsideAnArrayField() { /* mongo: {"a": {"$eq": null}} matches, as one element of the array is null, and From 16dfb46e0260662f35bb378caad82c2a20a53d43 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Tue, 8 Sep 2026 17:33:21 -0300 Subject: [PATCH 08/53] Refactor `$nin` heuristic evaluation; re-enable and extend related test cases --- .../mongo/MongoHeuristicsCalculator.java | 46 +++++++++++-------- .../mongo/MongoHeuristicsCalculatorTest.java | 41 ++++++++++++++++- 2 files changed, 68 insertions(+), 19 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java index 471e1a2c85..15f8b9f4ee 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java @@ -542,20 +542,7 @@ private Truthness computeHeuristic(InOperation operation, Object document) { actualValue = null; } - final Truthness res; - if (actualValue instanceof List) { - List actualValueList = (List) actualValue; - if (actualValueList.isEmpty()) { - res = C_FALSE; - } else { - Truthness orAggregation = buildOrAggregationTruthness(actualValueList.stream() - .map(value -> computeHeuristic(value, expectedValueList)) - .toArray(Truthness[]::new)); - res = buildSafeScaledTruthness(orAggregation); - } - } else { - res = computeHeuristic(actualValue, expectedValueList); - } + final Truthness res = computeInOperation(actualValue, expectedValueList); return res; } @@ -574,15 +561,36 @@ private Truthness computeHeuristic(Object actualValue, List expectedValueList private Truthness computeHeuristic(NotInOperation operation, Object document) { requireNonNullQueryAndDocument(operation, document); - List expectedValues = operation.getValues(); + List expectedValueList = operation.getValues(); final String fieldName = operation.getFieldName(); + final Object actualValue; if (!documentContainsField(document, fieldName)) { - return TRUE_C; + actualValue =null; } else { - Object actualValue = getValue(document, fieldName); - return computeHeuristic(actualValue, expectedValues).invert(); + actualValue = getValue(document, fieldName); } + + final Truthness res = computeInOperation(actualValue, expectedValueList); + return res.invert(); + } + + private Truthness computeInOperation(Object actualValue, List expectedValueList) { + final Truthness res; + if (actualValue instanceof List) { + List actualValueList = (List) actualValue; + if (actualValueList.isEmpty()) { + res = C_FALSE; + } else { + Truthness orAggregation = buildOrAggregationTruthness(actualValueList.stream() + .map(value -> computeHeuristic(value, expectedValueList)) + .toArray(Truthness[]::new)); + res = buildSafeScaledTruthness(orAggregation); + } + } else { + res = computeHeuristic(actualValue, expectedValueList); + } + return res; } private Truthness computeHeuristic(AllOperation operation, Object document) { @@ -631,6 +639,8 @@ private static boolean isSingleNullExpectedValue(List expectedValues) { } private static Truthness buildSafeScaledTruthness(Truthness truthness) { + Objects.requireNonNull(truthness); + return buildSafeScaledTruthness(truthness.getOfTrue()); } diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 7aadf62efd..51de8808f5 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -170,6 +170,16 @@ public void testIn() { Truthness distanceNotMatch = new MongoHeuristicsCalculator().computeHeuristicDocument(convertToDocument(bsonFalse), doc); assertTrue(distanceMatch.isTrue()); assertTrue(distanceNotMatch.isFalse()); + + Bson negBsonTrue = Filters.nin("age", new ArrayList<>(Arrays.asList(1, 10, 8))); + Bson negBsonFalse = Filters.nin("age", new ArrayList<>(Arrays.asList(1, 15))); + + Truthness negDistanceMatch = new MongoHeuristicsCalculator().computeHeuristicDocument(convertToDocument(negBsonTrue), doc); + Truthness negDistanceNotMatch = new MongoHeuristicsCalculator().computeHeuristicDocument(convertToDocument(negBsonFalse), doc); + + assertTrue(negDistanceMatch.isFalse()); + assertTrue(negDistanceNotMatch.isTrue()); + } @Test @@ -183,6 +193,16 @@ public void testInFieldMissingField() { assertTrue(distanceMatch.isTrue(), "Should match because null is in the list and undefined is treated as null"); assertTrue(distanceNotMatch.isFalse(), "Should not match because null is NOT in the list"); + + Bson negBsonTrue = Filters.nin("age", new ArrayList<>(Arrays.asList(null, 10, 8))); + Bson negBsonFalse = Filters.nin("age", new ArrayList<>(Arrays.asList(1, 15))); + + Truthness negDistanceMatch = new MongoHeuristicsCalculator().computeHeuristicDocument(convertToDocument(negBsonTrue), doc); + Truthness negDistanceNotMatch = new MongoHeuristicsCalculator().computeHeuristicDocument(convertToDocument(negBsonFalse), doc); + + assertTrue(negDistanceMatch.isFalse()); + assertTrue(negDistanceNotMatch.isTrue()); + } @Test @@ -196,6 +216,16 @@ public void testInFieldNull() { assertTrue(distanceMatch.isTrue(), "Should match because null is in the list"); assertTrue(distanceNotMatch.isFalse(), "Should not match because null is NOT in the list"); + + Bson negBsonTrue = Filters.nin("age", new ArrayList<>(Arrays.asList(null, 10, 8))); + Bson negBsonFalse = Filters.nin("age", new ArrayList<>(Arrays.asList(1, 15))); + + Truthness negDistanceMatch = new MongoHeuristicsCalculator().computeHeuristicDocument(convertToDocument(negBsonTrue), doc); + Truthness negDistanceNotMatch = new MongoHeuristicsCalculator().computeHeuristicDocument(convertToDocument(negBsonFalse), doc); + + assertTrue(negDistanceMatch.isFalse()); + assertTrue(negDistanceNotMatch.isTrue()); + } @Test @@ -211,6 +241,16 @@ public void testInFieldList() { assertTrue(distanceMatch.isTrue(), "Should match because 'b' is in both lists"); assertTrue(distanceNotMatch.isFalse(), "Should not match because no element is in both lists"); + + Bson negBsonTrue = Filters.nin("tags", new ArrayList<>(Arrays.asList("b", "z"))); + Bson negBsonFalse = Filters.nin("tags", new ArrayList<>(Arrays.asList("x", "y"))); + + Truthness negDistanceMatch = new MongoHeuristicsCalculator().computeHeuristicDocument(convertToDocument(negBsonTrue), doc); + Truthness negDistanceNotMatch = new MongoHeuristicsCalculator().computeHeuristicDocument(convertToDocument(negBsonFalse), doc); + + assertTrue(negDistanceMatch.isFalse(), "Should not match because 'b' is in the list"); + assertTrue(negDistanceNotMatch.isTrue(), "Should match because no element is in the list"); + } @Test @@ -1738,7 +1778,6 @@ public void testNotEqualsBetweenIncomparableTypes() { } @Test - @Disabled("$nin does not look at the elements of an array-valued field") public void testNotInAgainstAnArrayField() { Document doc = new Document().append("tags", new ArrayList<>(Arrays.asList("a", "b"))); MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); From f99a982d6ba3d53828712b0ebe96a1d582deccf1 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Tue, 8 Sep 2026 19:09:17 -0300 Subject: [PATCH 09/53] Refactor heuristic evaluation logic for `$in` and `$nin` operations --- .../mongo/MongoHeuristicsCalculator.java | 75 +++++++++++++------ .../mongo/MongoHeuristicsCalculatorTest.java | 13 +++- 2 files changed, 64 insertions(+), 24 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java index 15f8b9f4ee..ff0e2f4ffb 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java @@ -5,7 +5,6 @@ import org.evomaster.client.java.controller.mongo.geometry.GeoJsonUtils; import org.evomaster.client.java.controller.mongo.operations.*; import org.evomaster.client.java.controller.mongo.utils.BsonHelper; -import org.evomaster.client.java.controller.mongo.utils.MongoUtils; import org.evomaster.client.java.distance.heuristics.Truthness; import org.evomaster.client.java.instrumentation.coverage.methodreplacement.RegexDistanceUtils; import org.evomaster.client.java.sql.heuristic.SqlExpressionEvaluator; @@ -365,7 +364,7 @@ private Truthness computeHeuristic(EqualsOperation operation, Object document } if ((actualValue instanceof List) && !(expectedValue instanceof List)) { - return computeHeuristic(expectedValue, (List) actualValue); + return computeHeuristicContainsElement(expectedValue, (List) actualValue); } else { return computeHeuristicComparisonNullableValues( expectedValue, @@ -426,7 +425,7 @@ private Truthness computeHeuristic(NotEqualsOperation operation, Object docum actualValue = null; } if ((actualValue instanceof List) && !(expectedValue instanceof List)) { - return computeHeuristic(expectedValue, (List) actualValue).invert(); + return computeHeuristicContainsElement(expectedValue, (List) actualValue).invert(); } else { return computeHeuristicComparisonNullableValues( expectedValue, @@ -542,17 +541,29 @@ private Truthness computeHeuristic(InOperation operation, Object document) { actualValue = null; } - final Truthness res = computeInOperation(actualValue, expectedValueList); + final Truthness res = computeHeuristicInOperation(actualValue, expectedValueList); return res; } - private Truthness computeHeuristic(Object actualValue, List expectedValueList) { - Objects.requireNonNull(expectedValueList); - if (expectedValueList.isEmpty()) { + /** + * Computes the heuristic score for determining if an element is contained in a list. + * The method evaluates the presence of the given element in the provided list and calculates + * a heuristic truthness score based on the comparison results. + * + * @param element the element whose presence in the list is to be evaluated; can be null. + * @param list the list of objects to search; must not be null. + * @return a Truthness object representing the heuristic score of the element's presence in the list. + */ + private Truthness computeHeuristicContainsElement(Object element, List list) { + Objects.requireNonNull(list); + + if (list.isEmpty()) { return C_FALSE; } else { - Truthness res = buildOrAggregationTruthness(expectedValueList.stream() - .map(expectedValue -> computeHeuristicComparisonNullableValues(expectedValue, actualValue, SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO)) + Truthness res = buildOrAggregationTruthness(list.stream() + .map(expectedValue -> computeHeuristicComparisonNullableValues(expectedValue, + element, + SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO)) .toArray(Truthness[]::new)); return buildSafeScaledTruthness(res); } @@ -566,29 +577,48 @@ private Truthness computeHeuristic(NotInOperation operation, Object document) final Object actualValue; if (!documentContainsField(document, fieldName)) { - actualValue =null; + actualValue = null; } else { actualValue = getValue(document, fieldName); } - final Truthness res = computeInOperation(actualValue, expectedValueList); + final Truthness res = computeHeuristicInOperation(actualValue, expectedValueList); return res.invert(); } - private Truthness computeInOperation(Object actualValue, List expectedValueList) { + private Truthness computeHeuristicInOperation(Object actualValue, List expectedValueList) { final Truthness res; if (actualValue instanceof List) { List actualValueList = (List) actualValue; - if (actualValueList.isEmpty()) { - res = C_FALSE; + // first we try to match the actualValueList as a whole with any element of the expectedValueList + Truthness[] arrayOfTruthnesses = expectedValueList.stream() + .filter(expectedValueListElement -> expectedValueListElement instanceof List) + .map(expectedValueListElement -> (List) expectedValueListElement) + .map(expectedValueInnerListElement -> + computeHeuristicListEquality(expectedValueInnerListElement, actualValueList)) + .toArray(Truthness[]::new); + final Truthness isTheValueListEqualToAnyExpectedValueList; + if (arrayOfTruthnesses.length > 0) { + isTheValueListEqualToAnyExpectedValueList = buildOrAggregationTruthness(arrayOfTruthnesses); + } else { + isTheValueListEqualToAnyExpectedValueList = C_FALSE; + } + + if (isTheValueListEqualToAnyExpectedValueList.isFalse()) { + // if we fail, we try to match each element of the actualValueList with any element of the expectedValueList + if (actualValueList.isEmpty()) { + res= C_FALSE; + } else { + Truthness orAggregation = buildOrAggregationTruthness(actualValueList.stream() + .map(actualValueListElement -> computeHeuristicContainsElement(actualValueListElement, expectedValueList)) + .toArray(Truthness[]::new)); + res = buildSafeScaledTruthness(orAggregation); + } } else { - Truthness orAggregation = buildOrAggregationTruthness(actualValueList.stream() - .map(value -> computeHeuristic(value, expectedValueList)) - .toArray(Truthness[]::new)); - res = buildSafeScaledTruthness(orAggregation); + res = isTheValueListEqualToAnyExpectedValueList; } } else { - res = computeHeuristic(actualValue, expectedValueList); + res = computeHeuristicContainsElement(actualValue, expectedValueList); } return res; } @@ -629,7 +659,7 @@ private Truthness computeHeuristic(AllOperation operation, Object document) { } Truthness res = buildAndAggregationTruthness(expectedValues.stream() - .map(expectedValue -> computeHeuristic(expectedValue, actualValues)) + .map(expectedValue -> computeHeuristicContainsElement(expectedValue, actualValues)) .toArray(Truthness[]::new)); return buildSafeScaledTruthness(res); } @@ -859,7 +889,6 @@ private Truthness computeHeuristic(NearSphereOperation operation, Object documen } - private static Truthness computeHeuristic(AbstractProximityOperation abstractProximityOperation, double longitude, double latitude, @@ -912,7 +941,7 @@ private Truthness calculateTruthnessForListComparison(List actualList, List actualList, List actualList, List expectedList) { + private Truthness computeHeuristicListEquality(List actualList, List expectedList) { if (actualList.size() != expectedList.size()) { return C_FALSE; diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 51de8808f5..a0aa7ca2af 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -1847,7 +1847,6 @@ public void testAllOnAScalarField() { } @Test - @Disabled("$in and $nin compare only the elements of an array, never the array itself") public void testInMatchingAnArrayFieldAsAWhole() { Document doc = new Document().append("tags", new ArrayList<>(Arrays.asList("a", "b"))); MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); @@ -1859,6 +1858,18 @@ public void testInMatchingAnArrayFieldAsAWhole() { convertToDocument(Filters.nin("tags", Arrays.asList(Arrays.asList("a", "b")))), doc).isFalse()); } + @Test + public void testInMatchingAnEmptyArrayFieldAsAWhole() { + Document doc = new Document().append("tags", new ArrayList<>(Arrays.asList())); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + // mongo: matches, one of the listed values is the array itself + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.in("tags", Arrays.asList(Arrays.asList()))), doc).isTrue()); + assertTrue(calculator.computeHeuristicDocument( + convertToDocument(Filters.nin("tags", Arrays.asList(Arrays.asList()))), doc).isFalse()); + } + @Test public void testBitsDoesNotMatchANonIntegralNumber() { // mongo: 3.0 is an integer and is matched, 3.5 is not truncated to 3 From b876be326a77d6a60d99ef8ba17ea0d3226632cb Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Tue, 8 Sep 2026 19:22:09 -0300 Subject: [PATCH 10/53] Refactor `$regex` heuristic evaluation to handle array fields --- .../mongo/MongoHeuristicsCalculator.java | 37 ++++++++++++++++--- .../mongo/MongoHeuristicsCalculatorTest.java | 1 - 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java index ff0e2f4ffb..197c534aea 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java @@ -161,14 +161,39 @@ private Truthness computeHeuristic(RegexOperation operation, Object document) { Objects.requireNonNull(operation.getPattern()); Objects.requireNonNull(operation.getOptions()); - Object fieldValue = getValue(document, operation.getFieldName()); - if (!(fieldValue instanceof String)) { - return C_FALSE; + final String fieldName = operation.getFieldName(); + final Pattern pattern = operation.getPattern(); + + final Object actualValue; + if (documentContainsField(document, fieldName)) { + actualValue = getValue(document, fieldName); + } else { + actualValue = null; } - final String inputValue = (String) fieldValue; + if (actualValue == null) { + return C_FALSE; + } else if (actualValue instanceof String) { + final String inputValue = (String) actualValue; + return computeHeuristicRegularExpression(inputValue, pattern); + } else if (actualValue instanceof List) { + List actualValueList = (List) actualValue; + if (actualValueList.isEmpty()) { + return C_FALSE; + } else { + Truthness[] results = actualValueList.stream() + .filter(element -> element instanceof String) + .map(element -> (String) element) + .map(element -> computeHeuristicRegularExpression(element, pattern)) + .toArray(Truthness[]::new); + return buildOrAggregationTruthness(results); + } + } else { + return C_FALSE; + } + } - final Pattern pattern = operation.getPattern(); + private Truthness computeHeuristicRegularExpression(String inputValue, Pattern pattern) { final String patternString = pattern.pattern(); if (taintHandler != null) { @@ -607,7 +632,7 @@ private Truthness computeHeuristicInOperation(Object actualValue, List expect if (isTheValueListEqualToAnyExpectedValueList.isFalse()) { // if we fail, we try to match each element of the actualValueList with any element of the expectedValueList if (actualValueList.isEmpty()) { - res= C_FALSE; + res = C_FALSE; } else { Truthness orAggregation = buildOrAggregationTruthness(actualValueList.stream() .map(actualValueListElement -> computeHeuristicContainsElement(actualValueListElement, expectedValueList)) diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index a0aa7ca2af..718a12ef32 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -2032,7 +2032,6 @@ public void testOperatorsThatAreNotModelledDoNotThrow() { */ @Test - @Disabled("$regex does not look at the elements of an array-valued field") public void testRegexMatchingAnElementOfAnArrayField() { /* mongo: matches, one element of the array matches the expression. $regex follows From b499cdbe9f33e3f9d90ee4533d2289aff26b6cc8 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Tue, 8 Sep 2026 19:30:49 -0300 Subject: [PATCH 11/53] Refactor Mongo heuristic comparison to remove unsupported type checks; adjust comparison logic for `NOT_EQUALS_TO` operator. --- .../controller/mongo/MongoHeuristicsCalculator.java | 11 +++-------- .../mongo/MongoHeuristicsCalculatorTest.java | 1 - 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java index 197c534aea..a322e59013 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java @@ -252,12 +252,6 @@ private Truthness computeHeuristic(TrueOperation operation, Object document) { private Truthness computeHeuristicComparisonNonNullValues(Object actualValue, Object expectedValue, SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType) { Objects.requireNonNull(actualValue); Objects.requireNonNull(expectedValue); - if (!isTypeSupportedForComparison(actualValue)) { - throw new IllegalArgumentException("Unsupported type: " + actualValue.getClass().getName()); - } - if (!isTypeSupportedForComparison(expectedValue)) { - throw new IllegalArgumentException("Unsupported type: " + expectedValue.getClass().getName()); - } final Truthness truthnessOfComparison; if (actualValue instanceof Number && expectedValue instanceof Number) { @@ -301,8 +295,9 @@ private Truthness computeHeuristicComparisonNonNullValues(Object actualValue, Ob } else { // If both types are supported, but no actual comparison logic is defined, - // we considered them to be incompatible, therefore the comparison returns false. - truthnessOfComparison = C_FALSE; + // we considered them to be incompatible, therefore the comparison returns true + // only if the comparison operator is NOT_EQUALS_TO. Otherwise returns false. + truthnessOfComparison = comparisonOperatorType == SqlExpressionEvaluator.ComparisonOperatorType.NOT_EQUALS_TO ? TRUE_C : C_FALSE; } return truthnessOfComparison; } diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 718a12ef32..b6fef1d19d 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -1713,7 +1713,6 @@ public void testEqualsBetweenEmptyArrays() { } @Test - @Disabled("a value the calculator cannot compare makes it throw IllegalArgumentException") public void testFieldsHoldingASubDocument() { /* mongo: does not fail on any of these, it simply does not match. A sub-document From 820b48cbad2adc4e0f3e4abc1e96f8ad67b7e959 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Tue, 8 Sep 2026 19:31:54 -0300 Subject: [PATCH 12/53] Re-enable test for `$ne` between incomparable BSON types; remove outdated `@Disabled` annotation. --- .../java/controller/mongo/MongoHeuristicsCalculatorTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index b6fef1d19d..93e7ba9899 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -1765,7 +1765,6 @@ public void testAllQuantifiesOverTheExpectedValues() { } @Test - @Disabled("$ne between values of incomparable BSON types is reported as false") public void testNotEqualsBetweenIncomparableTypes() { // mongo: matches, a number and a string are different from each other MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); From 445e4533061f4b2890a9f124fb4bdef2bc54eb0c Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Tue, 8 Sep 2026 20:01:12 -0300 Subject: [PATCH 13/53] fix parsing logic for `$not` with multiple comparison operators --- .../mongo/selectors/NotSelector.java | 9 ++-- .../mongo/MongoHeuristicsCalculatorTest.java | 1 - .../controller/mongo/QueryParserTest.java | 41 ++++++++++++++++++- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/NotSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/NotSelector.java index b93390274d..f8d010ede1 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/NotSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/NotSelector.java @@ -22,14 +22,12 @@ public class NotSelector extends SingleConditionQuerySelector { protected QueryOperation parseValue(String fieldName, Object value) { if (isBsonDocument(value)) { // A $not cannot contain $and, $or, $nor, etc. (logical operators at field level) - // or other $not Set keys = documentKeys(value); if (keys == null || keys.isEmpty() || keys.stream().anyMatch(key -> !key.startsWith("$"))) { return null; } String innerOp = keys.iterator().next(); - if (innerOp.equals(NOT_OPERATOR) - || innerOp.equals(AND_OPERATOR) + if (innerOp.equals(AND_OPERATOR) || innerOp.equals(OR_OPERATOR) || innerOp.equals(NOR_OPERATOR)) { return null; @@ -41,9 +39,8 @@ protected QueryOperation parseValue(String fieldName, Object value) { appendToDocument(docWithRemovedNot, fieldName, value); QueryOperation condition = new QueryParser().parse(docWithRemovedNot); - if (condition == null || condition instanceof AndOperation) { - // If it parsed as AndOperation (because of ImplicitSelector), it's probably wrong here - // or if it failed to parse. + if (condition == null) { + // If it failed to parse. return null; } else { return new NotOperation(fieldName, condition); diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 93e7ba9899..c33ed19a1b 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -2046,7 +2046,6 @@ public void testRegexMatchingAnElementOfAnArrayField() { } @Test - @Disabled("$not holding more than one operator is not parsed, so the calculator throws a NullPointerException") public void testNotWithMoreThanOneInnerOperator() { /* mongo: {"a": {"$not": {"$gt": 1, "$lt": 9}}} matches {a:1}, as the two inner diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java index 5950cf21b4..a141da679b 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java @@ -224,7 +224,6 @@ void testParseNestedElemMatch() { } - @Test void testParseElemMatchWithMultipleConditions() { Document query = new Document( @@ -1458,6 +1457,46 @@ void testParseNotWithComparisonOperator() { assertEquals(18, ((GreaterThanOperation) not.getCondition()).getValue()); } + @Test + void testParseValidNestedNotQuery() { + Document query = new Document( + "a", + new Document("$not", new Document("$not", new Document("$gt", 1))) + ); + + QueryOperation operation = parser.parse(query); + + assertNotNull(operation); + assertTrue(operation instanceof NotOperation); + NotOperation outerNot = (NotOperation) operation; + assertEquals("a", outerNot.getFieldName()); + assertTrue(outerNot.getCondition() instanceof NotOperation); + + NotOperation innerNot = (NotOperation) outerNot.getCondition(); + assertEquals("a", innerNot.getFieldName()); + assertTrue(innerNot.getCondition() instanceof GreaterThanOperation); + assertEquals(1, ((GreaterThanOperation) innerNot.getCondition()).getValue()); + } + + @Test + void testParseInvalidNotWithMultipleComparisonOperators() { + Document query = new Document( + "a", + new Document("$not", new Document("$gt", 1).append("$lt", 9)) + ); + + QueryOperation operation = parser.parse(query); + assertNotNull(operation); + assertTrue(operation instanceof NotOperation); + NotOperation not = (NotOperation) operation; + assertEquals("a", not.getFieldName()); + assertTrue(not.getCondition() instanceof AndOperation); + AndOperation and = (AndOperation) not.getCondition(); + assertEquals(2, and.getConditions().size()); + assertTrue(and.getConditions().get(0) instanceof GreaterThanOperation); + assertTrue(and.getConditions().get(1) instanceof LessThanOperation); + } + @Test void testParseTopLevelValueOperatorWithSyntheticField() { QueryOperation operation = parser.parse(new Document("$gt", 18)); From ca4d299188730d372be6e828eb20eb9607ac6b02 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Tue, 8 Sep 2026 20:05:00 -0300 Subject: [PATCH 14/53] Refactor `$not` heuristic calculation; remove field existence check and simplify logic --- .../controller/mongo/MongoHeuristicsCalculator.java | 10 ++-------- .../mongo/MongoHeuristicsCalculatorTest.java | 1 - 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java index a322e59013..55d9985c39 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java @@ -856,14 +856,8 @@ private Truthness computeHeuristic(BitsOperation operation, Object document) { private Truthness computeHeuristic(NotOperation operation, Object document) { requireNonNullQueryAndDocument(operation, document); - - String fieldName = operation.getFieldName(); - if (!documentContainsField(document, fieldName)) { - return TRUE_C; - } else { - QueryOperation condition = operation.getCondition(); - return computeHeuristicOnDocument(condition, document).invert(); - } + QueryOperation condition = operation.getCondition(); + return computeHeuristicOnDocument(condition, document).invert(); } private Truthness computeHeuristic(NorOperation operation, Object document) { diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index c33ed19a1b..4bdee54fd2 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -1789,7 +1789,6 @@ public void testNotInAgainstAnArrayField() { } @Test - @Disabled("$not answers true whenever the field is absent, overriding the inner operator") public void testNotOnAMissingFieldWithAnInnerOperatorThatMatchesIt() { /* mongo: no match for any of these. $ne, $nin and $exists:false all match a From ce4f8041dfd15d96f81a24f6002abf692faa67a0 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Tue, 8 Sep 2026 20:38:05 -0300 Subject: [PATCH 15/53] Refactor `TrueOperation` to `EmptyOperation`; update query parsing logic to handle `$comment` operators --- .../mongo/MongoHeuristicsCalculator.java | 6 +- .../java/controller/mongo/QueryParser.java | 34 +++++- ...TrueOperation.java => EmptyOperation.java} | 4 +- .../mongo/selectors/ImplicitSelector.java | 2 +- .../mongo/MongoHeuristicsCalculatorTest.java | 1 - .../controller/mongo/QueryParserTest.java | 115 ++++++++++++++++-- 6 files changed, 145 insertions(+), 17 deletions(-) rename client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/operations/{TrueOperation.java => EmptyOperation.java} (73%) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java index 55d9985c39..10790c0653 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java @@ -149,8 +149,8 @@ private Truthness computeHeuristicOnDocument(QueryOperation operation, Object do return computeHeuristic((NearOperation) operation, document); } else if (operation instanceof ElemMatchOperation) { return computeHeuristic((ElemMatchOperation) operation, document); - } else if (operation instanceof TrueOperation) { - return computeHeuristic((TrueOperation) operation, document); + } else if (operation instanceof EmptyOperation) { + return computeHeuristic((EmptyOperation) operation, document); } else { throw new IllegalArgumentException("Unsupported QueryOperation type: " + operation.getClass().getName()); } @@ -244,7 +244,7 @@ private Truthness computeHeuristic(NearOperation operation, Object document) { * @param document * @return */ - private Truthness computeHeuristic(TrueOperation operation, Object document) { + private Truthness computeHeuristic(EmptyOperation operation, Object document) { requireNonNullQueryAndDocument(operation, document); return TRUE_C; } diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/QueryParser.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/QueryParser.java index 0b75c2f5d6..50083defe2 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/QueryParser.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/QueryParser.java @@ -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; @@ -19,6 +21,7 @@ public class QueryParser { private static final String SYNTHETIC_FIELD_NAME = "$"; + private static final Set COMMENTS_OPERATORS = new HashSet<>(Arrays.asList("$comment", "$comments")); List selectors = Arrays.asList( new EqualsSelector(), @@ -54,18 +57,43 @@ public QueryOperation parse(Object bsonDocument) { return null; } - QueryOperation operation = parseWithSelectors(bsonDocument); + Object normalizedWithoutComments = 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); } + 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 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; diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/operations/TrueOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/operations/EmptyOperation.java similarity index 73% rename from client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/operations/TrueOperation.java rename to client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/operations/EmptyOperation.java index eaf289a960..6d21721f83 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/operations/TrueOperation.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/operations/EmptyOperation.java @@ -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() { } } diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ImplicitSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ImplicitSelector.java index f421a7fbd6..377f28f6c9 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ImplicitSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ImplicitSelector.java @@ -27,7 +27,7 @@ public QueryOperation getOperation(Object query) { Set keys = documentKeys(query); if (keys.isEmpty()) { - return new TrueOperation(); // Represents the "{}" MongoDB query, which matches all documents + return new EmptyOperation(); // Represents the "{}" MongoDB query, which matches all documents } if (!isUniqueEntry((Map) query)) { diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 4bdee54fd2..a33f435174 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -1999,7 +1999,6 @@ public void testOperatorsThatAreNotModelledDoNotThrow() { MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); List queries = Arrays.asList( - new Document().append("a", 1).append("$comment", "a comment"), new Document().append("$expr", new Document().append("$eq", Arrays.asList("$a", 1))), new Document().append("$jsonSchema", diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java index a141da679b..6781601faf 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java @@ -9,10 +9,7 @@ import org.bson.types.Decimal128; import org.evomaster.client.java.controller.mongo.operations.*; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -1052,7 +1049,7 @@ void testParseImplicitEqualsWithMultipleFields() { void testParseImplicitEqualsWithEmptyDocument() { Document query = new Document(); QueryOperation operation = parser.parse(query); - assertTrue(operation instanceof TrueOperation); + assertTrue(operation instanceof EmptyOperation); } @Test @@ -1377,7 +1374,7 @@ void testParseTrueOperation() { QueryOperation operation = parser.parse(query); assertNotNull(operation); - assertTrue(operation instanceof TrueOperation); + assertTrue(operation instanceof EmptyOperation); } @@ -1393,7 +1390,7 @@ void testParseNorTrueOperation() { assertTrue(operation instanceof NorOperation); NorOperation nor = (NorOperation) operation; assertEquals(1, nor.getConditions().size()); - assertTrue(nor.getConditions().get(0) instanceof TrueOperation); + assertTrue(nor.getConditions().get(0) instanceof EmptyOperation); } @Test @@ -1434,6 +1431,110 @@ void testParseLessThan() { assertEquals(65, lessThan.getValue()); } + @Test + void testParseAttachedComments() { + Document query = new Document("age", 42).append("$comments", "a comment"); + QueryOperation operation = parser.parse(query); + + assertTrue(operation instanceof EqualsOperation); + EqualsOperation eq = (EqualsOperation) operation; + assertEquals("age", eq.getFieldName()); + assertEquals(42, eq.getValue()); + } + + @Test + void testParseOnlyComments() { + Document query = new Document("$comments", "a comment"); + QueryOperation operation = parser.parse(query); + + assertTrue(operation instanceof EmptyOperation); + } + + @Test + void testParseOnlyComment() { + Document query = new Document("$comment", "a comment"); + QueryOperation operation = parser.parse(query); + + assertTrue(operation instanceof EmptyOperation); + } + + @Test + void testParseAndWithOnlyCommentElement() { + Document query = new Document( + "$and", + Collections.singletonList(new Document("$comment", "a comment")) + ); + + QueryOperation operation = parser.parse(query); + + assertTrue(operation instanceof AndOperation); + AndOperation and = (AndOperation) operation; + assertEquals(1, and.getConditions().size()); + assertTrue(and.getConditions().get(0) instanceof EmptyOperation); + } + + @Test + void testParseNorWithOnlyCommentElement() { + Document query = new Document( + "$nor", + Collections.singletonList(new Document("$comment", "a comment")) + ); + + QueryOperation operation = parser.parse(query); + + assertTrue(operation instanceof NorOperation); + NorOperation norOperation = (NorOperation) operation; + assertEquals(1, norOperation.getConditions().size()); + assertTrue(norOperation.getConditions().get(0) instanceof EmptyOperation); + } + + @Test + void testParseAttachedCommentInsideFieldOperatorDocument() { + Document query = new Document( + "age", + new Document("$gt", 18).append("$comments", "a comment") + ); + + QueryOperation operation = parser.parse(query); + + assertTrue(operation instanceof GreaterThanOperation); + GreaterThanOperation gt = (GreaterThanOperation) operation; + assertEquals("age", gt.getFieldName()); + assertEquals(18, gt.getValue()); + } + + @Test + void testParseCommentsIgnoredRecursivelyInLogicalQuery() { + Document query = new Document( + "$and", + Arrays.asList( + new Document("age", new Document("$gt", 18).append("$comments", "inner comment")), + new Document( + "$or", + Arrays.asList( + new Document("name", "John").append("$comments", "leaf comment"), + new Document("name", "Jane") + ) + ).append("$comments", "or comment"), + new Document("$comments", "no-op comment") + ) + ).append("$comments", "top-level comment"); + + QueryOperation operation = parser.parse(query); + + assertTrue(operation instanceof AndOperation); + AndOperation and = (AndOperation) operation; + assertEquals(3, and.getConditions().size()); + assertTrue(and.getConditions().get(0) instanceof GreaterThanOperation); + assertTrue(and.getConditions().get(1) instanceof OrOperation); + assertTrue(and.getConditions().get(2) instanceof EmptyOperation); + + OrOperation or = (OrOperation) and.getConditions().get(1); + assertEquals(2, or.getConditions().size()); + assertTrue(or.getConditions().get(0) instanceof EqualsOperation); + assertTrue(or.getConditions().get(1) instanceof EqualsOperation); + } + @Test void testParseExistsFalse() { QueryOperation operation = parser.parse( @@ -1648,7 +1749,7 @@ void testParseElemMatchWithEmptyCondition() { ); assertTrue(operation instanceof ElemMatchOperation); - assertTrue(((ElemMatchOperation) operation).getCondition() instanceof TrueOperation); + assertTrue(((ElemMatchOperation) operation).getCondition() instanceof EmptyOperation); } @Test From 9282ade87c741b1c01d8e760e6ac9ea758776dff Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Tue, 8 Sep 2026 20:55:26 -0300 Subject: [PATCH 16/53] Refactor heuristic calculation to handle arrays in comparison operations --- .../mongo/MongoHeuristicsCalculator.java | 80 +++++++++++++------ .../mongo/MongoHeuristicsCalculatorTest.java | 1 - 2 files changed, 57 insertions(+), 24 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java index 10790c0653..df8bd2f59d 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java @@ -18,6 +18,7 @@ import java.util.*; +import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.StreamSupport; @@ -465,10 +466,11 @@ private Truthness computeHeuristic(GreaterThanOperation operation, Object doc return C_FALSE; } else { Object actualValue = getValue(document, fieldName); - return computeHeuristicComparisonNullableValues( - expectedValue, - actualValue, - SqlExpressionEvaluator.ComparisonOperatorType.GREATER_THAN); + return computeHeuristicForActualValueOrAnyElement(actualValue, + value -> computeHeuristicComparisonNullableValues( + expectedValue, + value, + SqlExpressionEvaluator.ComparisonOperatorType.GREATER_THAN)); } } @@ -483,10 +485,11 @@ private Truthness computeHeuristic(GreaterThanEqualsOperation operation, Obje return C_FALSE; } else { Object actualValue = getValue(document, fieldName); - return computeHeuristicComparisonNullableValues( - expectedValue, - actualValue, - SqlExpressionEvaluator.ComparisonOperatorType.GREATER_THAN_EQUALS); + return computeHeuristicForActualValueOrAnyElement(actualValue, + value -> computeHeuristicComparisonNullableValues( + expectedValue, + value, + SqlExpressionEvaluator.ComparisonOperatorType.GREATER_THAN_EQUALS)); } } @@ -501,10 +504,11 @@ private Truthness computeHeuristic(LessThanOperation operation, Object docume return C_FALSE; } else { Object actualValue = getValue(document, fieldName); - return computeHeuristicComparisonNullableValues( - expectedValue, - actualValue, - SqlExpressionEvaluator.ComparisonOperatorType.MINOR_THAN); + return computeHeuristicForActualValueOrAnyElement(actualValue, + value -> computeHeuristicComparisonNullableValues( + expectedValue, + value, + SqlExpressionEvaluator.ComparisonOperatorType.MINOR_THAN)); } } @@ -518,14 +522,34 @@ private Truthness computeHeuristic(LessThanEqualsOperation operation, Object return C_FALSE; } else { Object actualValue = getValue(document, fieldName); - return computeHeuristicComparisonNullableValues( - expectedValue, - actualValue, - SqlExpressionEvaluator.ComparisonOperatorType.MINOR_THAN_EQUALS); + return computeHeuristicForActualValueOrAnyElement(actualValue, + value -> computeHeuristicComparisonNullableValues( + expectedValue, + value, + SqlExpressionEvaluator.ComparisonOperatorType.MINOR_THAN_EQUALS)); } } + private Truthness computeHeuristicForActualValueOrAnyElement(Object actualValue, + Function elementHeuristic) { + Objects.requireNonNull(elementHeuristic); + + if (!(actualValue instanceof List)) { + return elementHeuristic.apply(actualValue); + } + + List values = (List) actualValue; + if (values.isEmpty()) { + return C_FALSE; + } + + Truthness orAggregation = buildOrAggregationTruthness(values.stream() + .map(elementHeuristic) + .toArray(Truthness[]::new)); + return buildSafeScaledTruthness(orAggregation); + } + private Truthness computeHeuristic(OrOperation operation, Object document) { requireNonNullQueryAndDocument(operation, document); @@ -806,13 +830,18 @@ private Truthness computeHeuristic(ModOperation operation, Object document) { actualValue = getValue(document, fieldName); } - if (actualValue == null || !(actualValue instanceof Number)) { + return computeHeuristicForActualValueOrAnyElement(actualValue, + value -> computeHeuristicModOnSingleValue(value, divisor, expectedRemainder)); + } + + private Truthness computeHeuristicModOnSingleValue(Object value, long divisor, long expectedRemainder) { + if (!(value instanceof Number)) { return C_FALSE; - } else { - long actualRemainder = ((Number) actualValue).longValue() % divisor; - Truthness res = getEqualityTruthness(actualRemainder, expectedRemainder); - return buildSafeScaledTruthness(res); } + + long actualRemainder = ((Number) value).longValue() % divisor; + Truthness res = getEqualityTruthness(actualRemainder, expectedRemainder); + return buildSafeScaledTruthness(res); } private Truthness computeHeuristic(BitsOperation operation, Object document) { @@ -824,11 +853,16 @@ private Truthness computeHeuristic(BitsOperation operation, Object document) { } Object actualValue = getValue(document, fieldName); - if (!(actualValue instanceof Number)) { + return computeHeuristicForActualValueOrAnyElement(actualValue, + value -> computeHeuristicBitsOnSingleValue(value, operation)); + } + + private Truthness computeHeuristicBitsOnSingleValue(Object value, BitsOperation operation) { + if (!(value instanceof Number)) { return C_FALSE; } - final OptionalLong integralValue = getIntegralLongValue((Number) actualValue); + final OptionalLong integralValue = getIntegralLongValue((Number) value); if (!integralValue.isPresent()) { return C_FALSE; } diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index a33f435174..27ec65ff8b 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -1806,7 +1806,6 @@ public void testNotOnAMissingFieldWithAnInnerOperatorThatMatchesIt() { } @Test - @Disabled("a field is matched only as a whole, never by an element of the array it holds") public void testFieldsAreMatchedByAnElementOfTheArrayTheyHold() { /* mongo matches all of these. A field holding an array satisfies a condition when From afe42f4a9894ecd44bf83362872ef05ba66e8d15 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Fri, 11 Sep 2026 15:10:01 -0300 Subject: [PATCH 17/53] Add additional unit tests for Mongo heuristic operations and refactor handling of query operations --- .../mongo/MongoHeuristicsCalculator.java | 647 +++++++----------- .../MongoHeuristicsCalculatorHelper.java | 79 +++ .../mongo/MongoHeuristicsCalculatorTest.java | 51 +- 3 files changed, 393 insertions(+), 384 deletions(-) create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java index df8bd2f59d..5d4082286b 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java @@ -6,31 +6,23 @@ import org.evomaster.client.java.controller.mongo.operations.*; import org.evomaster.client.java.controller.mongo.utils.BsonHelper; import org.evomaster.client.java.distance.heuristics.Truthness; -import org.evomaster.client.java.instrumentation.coverage.methodreplacement.RegexDistanceUtils; import org.evomaster.client.java.sql.heuristic.SqlExpressionEvaluator; import org.evomaster.client.java.sql.internal.TaintHandler; +import static org.evomaster.client.java.controller.mongo.MongoHeuristicsCalculatorHelper.*; import static org.evomaster.client.java.controller.mongo.utils.BsonHelper.*; import static org.evomaster.client.java.controller.mongo.utils.MongoUtils.*; import static org.evomaster.client.java.controller.mongo.utils.MongoUtils.GeoSpatialModel.SPHERICAL; import static org.evomaster.client.java.distance.heuristics.TruthnessUtils.*; import static org.evomaster.client.java.sql.heuristic.ConversionHelper.convertToInstant; - import java.util.*; import java.util.function.Function; -import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.StreamSupport; public class MongoHeuristicsCalculator { - // TODO these constants should be replaced by DistanceHelper constants - public static final double C = 0.1; - // TODO These constants should be refactored by TruthnessUtils constants - public static final Truthness TRUE_C = new Truthness(1.0, C); - public static final Truthness C_FALSE = new Truthness(C, 1.0); - private final TaintHandler taintHandler; @@ -69,7 +61,7 @@ private Truthness computeHeuristicOnDocuments(QueryOperation operation, Iterable double maxOfTrue = 0; boolean first = true; for (Object doc : documents) { - double ofTrue = computeHeuristicOnDocument(operation, doc).getOfTrue(); + double ofTrue = computeHeuristicQueryOperation(operation, doc).getOfTrue(); if (first || ofTrue > maxOfTrue) { maxOfTrue = ofTrue; } @@ -79,14 +71,6 @@ private Truthness computeHeuristicOnDocuments(QueryOperation operation, Iterable return buildSafeScaledTruthness(maxOfTrue); } - private static Truthness buildSafeScaledTruthness(double maxOfTrue) { - if (maxOfTrue == 1.0) { - return TRUE_C; - } else { - return buildScaledTruthness(C, maxOfTrue); - } - } - /** * Compute a "branch" distance heuristics. * @@ -96,87 +80,106 @@ private static Truthness buildSafeScaledTruthness(double maxOfTrue) { */ Truthness computeHeuristicDocument(Object query, Object document) { QueryOperation operation = parseQuery(query); - return computeHeuristicOnDocument(operation, document); + return computeHeuristicQueryOperation(operation, document); } private QueryOperation parseQuery(Object query) { return new QueryParser().parse(query); } - private Truthness computeHeuristicOnDocument(QueryOperation operation, Object document) { - requireNonNullQueryAndDocument(operation, document); + private Truthness computeHeuristicQueryOperation(QueryOperation operation, Object value) { + Objects.requireNonNull(operation); if (operation instanceof AndOperation) { - return computeHeuristic((AndOperation) operation, document); + return computeHeuristic((AndOperation) operation, value); } else if (operation instanceof OrOperation) { - return computeHeuristic((OrOperation) operation, document); + return computeHeuristic((OrOperation) operation, value); } else if (operation instanceof NorOperation) { - return computeHeuristic((NorOperation) operation, document); - } else if (operation instanceof ExistsOperation) { + return computeHeuristic((NorOperation) operation, value); + } else if (operation instanceof EmptyOperation) { + return computeHeuristic((EmptyOperation) operation, value); + } else if (operation instanceof QueryOperationWithField) { + return computeHeuristic((QueryOperationWithField) operation, value); + } else { + throw new IllegalArgumentException("Unsupported QueryOperation type: " + operation.getClass().getName()); + } + } + + private Truthness computeHeuristic(QueryOperationWithField operation, Object document) { + Objects.requireNonNull(operation); + + if (operation instanceof ExistsOperation) { return computeHeuristic((ExistsOperation) operation, document); - } else if (operation instanceof EqualsOperation) { - return computeHeuristic((EqualsOperation) operation, document); + } else if (operation instanceof TypeOperation) { + return computeHeuristic((TypeOperation) operation, document); + } else if (operation instanceof NotOperation) { + return computeHeuristicQueryOperation(((NotOperation) operation).getCondition(), document).invert(); + } else { + final String fieldName = operation.getFieldName(); + final Object actualValue; + if (!fieldName.equalsIgnoreCase("$")) { + if (!isBsonDocument(document)) { + return C_FALSE; // cannot extract the field from a non-document value + } else { + actualValue = documentContainsField(document, fieldName) ? getValue(document, fieldName) : null; + } + } else { + actualValue = document; + } + return evaluate(operation, actualValue); + } + } + + private Truthness evaluate(QueryOperation operation, Object actualValue) { + if (operation instanceof EqualsOperation) { + return evaluate((EqualsOperation) operation, actualValue); } else if (operation instanceof NotEqualsOperation) { - return computeHeuristic((NotEqualsOperation) operation, document); + return evaluate((NotEqualsOperation) operation, actualValue); } else if (operation instanceof GreaterThanOperation) { - return computeHeuristic((GreaterThanOperation) operation, document); + return evaluate((GreaterThanOperation) operation, actualValue); } else if (operation instanceof GreaterThanEqualsOperation) { - return computeHeuristic((GreaterThanEqualsOperation) operation, document); + return evaluate((GreaterThanEqualsOperation) operation, actualValue); } else if (operation instanceof LessThanOperation) { - return computeHeuristic((LessThanOperation) operation, document); + return evaluate((LessThanOperation) operation, actualValue); } else if (operation instanceof LessThanEqualsOperation) { - return computeHeuristic((LessThanEqualsOperation) operation, document); + return evaluate((LessThanEqualsOperation) operation, actualValue); } else if (operation instanceof InOperation) { - return computeHeuristic((InOperation) operation, document); + return evaluate((InOperation) operation, actualValue); } else if (operation instanceof NotInOperation) { - return computeHeuristic((NotInOperation) operation, document); + return evaluate((NotInOperation) operation, actualValue); } else if (operation instanceof AllOperation) { - return computeHeuristic((AllOperation) operation, document); + return evaluate((AllOperation) operation, actualValue); } else if (operation instanceof SizeOperation) { - return computeHeuristic((SizeOperation) operation, document); + return evaluate((SizeOperation) operation, actualValue); } else if (operation instanceof ModOperation) { - return computeHeuristic((ModOperation) operation, document); + return evaluate((ModOperation) operation, actualValue); } else if (operation instanceof BitsOperation) { - return computeHeuristic((BitsOperation) operation, document); + return evaluate((BitsOperation) operation, actualValue); } else if (operation instanceof NotOperation) { - return computeHeuristic((NotOperation) operation, document); - } else if (operation instanceof TypeOperation) { - return computeHeuristic((TypeOperation) operation, document); + return computeHeuristicOnValue((NotOperation) operation, actualValue); } else if (operation instanceof RegexOperation) { - return computeHeuristic((RegexOperation) operation, document); + return evaluate((RegexOperation) operation, actualValue); } else if (operation instanceof NearSphereOperation) { - return computeHeuristic((NearSphereOperation) operation, document); + return evaluate((NearSphereOperation) operation, actualValue); } else if (operation instanceof NearOperation) { - return computeHeuristic((NearOperation) operation, document); + return evaluate((NearOperation) operation, actualValue); } else if (operation instanceof ElemMatchOperation) { - return computeHeuristic((ElemMatchOperation) operation, document); - } else if (operation instanceof EmptyOperation) { - return computeHeuristic((EmptyOperation) operation, document); + return evaluate((ElemMatchOperation) operation, actualValue); } else { throw new IllegalArgumentException("Unsupported QueryOperation type: " + operation.getClass().getName()); } + } - private Truthness computeHeuristic(RegexOperation operation, Object document) { - requireNonNullQueryAndDocument(operation, document); - Objects.requireNonNull(operation.getPattern()); - Objects.requireNonNull(operation.getOptions()); + private Truthness evaluate(RegexOperation operation, Object actualValue) { + Objects.requireNonNull(operation); - final String fieldName = operation.getFieldName(); final Pattern pattern = operation.getPattern(); - - final Object actualValue; - if (documentContainsField(document, fieldName)) { - actualValue = getValue(document, fieldName); - } else { - actualValue = null; - } - if (actualValue == null) { return C_FALSE; } else if (actualValue instanceof String) { final String inputValue = (String) actualValue; - return computeHeuristicRegularExpression(inputValue, pattern); + return evaluateRegularExpression(inputValue, pattern, taintHandler); } else if (actualValue instanceof List) { List actualValueList = (List) actualValue; if (actualValueList.isEmpty()) { @@ -185,7 +188,7 @@ private Truthness computeHeuristic(RegexOperation operation, Object document) { Truthness[] results = actualValueList.stream() .filter(element -> element instanceof String) .map(element -> (String) element) - .map(element -> computeHeuristicRegularExpression(element, pattern)) + .map(element -> evaluateRegularExpression(element, pattern, taintHandler)) .toArray(Truthness[]::new); return buildOrAggregationTruthness(results); } @@ -194,39 +197,8 @@ private Truthness computeHeuristic(RegexOperation operation, Object document) { } } - private Truthness computeHeuristicRegularExpression(String inputValue, Pattern pattern) { - final String patternString = pattern.pattern(); - - if (taintHandler != null) { - final int patternFlags = pattern.flags(); - // TODO: tainting should take into account the pattern flags, which can change the matching behavior - // TODO: regex could be a partial word match (MongoDB $regex) instead of a whole word match (Matcher.matches()) - taintHandler.handleTaintForRegex(inputValue, patternString); - } - - - Matcher matcher = pattern.matcher(inputValue); - boolean matches = matcher.find(); - - if (matches) { - return TRUE_C; - } else { - // TODO this does not take into account pattern flags, which can change the matching behavior - final int distance = RegexDistanceUtils.getStandardDistance(inputValue, patternString); - // The distance approximation can be zero even when Java's matcher rejects the input - // (for example, when flags affect line terminators). Keep non-matches strictly false. - double ofTrue = 1d / (1.1d + distance); - return buildSafeScaledTruthness(ofTrue); - } - } - - private Truthness computeHeuristic(NearOperation operation, Object document) { + private Truthness evaluate(NearOperation operation, Object actualValue) { Objects.requireNonNull(operation); - Objects.requireNonNull(document); - Objects.requireNonNull(operation.getFieldName()); - - final String fieldName = operation.getFieldName(); - final Object fieldValue = getValue(document, fieldName); final double longitude = operation.getLongitude(); final double latitude = operation.getLatitude(); @@ -234,7 +206,7 @@ private Truthness computeHeuristic(NearOperation operation, Object document) { GeoSpatialModel model = operation.hasLegacyCoordinates() ? GeoSpatialModel.PLANAR : SPHERICAL; - return computeHeuristic(operation, longitude, latitude, fieldValue, model); + return evaluate(operation, longitude, latitude, actualValue, model); } /** @@ -250,45 +222,45 @@ private Truthness computeHeuristic(EmptyOperation operation, Object document) { return TRUE_C; } - private Truthness computeHeuristicComparisonNonNullValues(Object actualValue, Object expectedValue, SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType) { - Objects.requireNonNull(actualValue); - Objects.requireNonNull(expectedValue); + private Truthness compareNonNullValues(Object leftValue, SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType, Object rightValue) { + Objects.requireNonNull(leftValue); + Objects.requireNonNull(rightValue); final Truthness truthnessOfComparison; - if (actualValue instanceof Number && expectedValue instanceof Number) { - final Number actualNumber = (Number) actualValue; - final Number expectedNumber = (Number) expectedValue; - truthnessOfComparison = computeHeuristicComparisonNumberValues(actualNumber, expectedNumber, comparisonOperatorType); - - } else if (actualValue instanceof String && expectedValue instanceof String) { - String actualString = (String) actualValue; - String expectedString = (String) expectedValue; + if (leftValue instanceof Number && rightValue instanceof Number) { + final Number actualNumber = (Number) leftValue; + final Number expectedNumber = (Number) rightValue; + truthnessOfComparison = compareNumberValues(actualNumber, comparisonOperatorType, expectedNumber); + + } else if (leftValue instanceof String && rightValue instanceof String) { + String actualString = (String) leftValue; + String expectedString = (String) rightValue; if (taintHandler != null && comparisonOperatorType == SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO) { taintHandler.handleTaintForStringEquals(actualString, expectedString, false); } truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForStringComparison(actualString, expectedString, comparisonOperatorType); - } else if (actualValue instanceof Boolean && expectedValue instanceof Boolean) { - int actualIntValue = toIntValue((Boolean) actualValue); - int expectedIntValue = toIntValue((Boolean) expectedValue); + } else if (leftValue instanceof Boolean && rightValue instanceof Boolean) { + int actualIntValue = toIntValue((Boolean) leftValue); + int expectedIntValue = toIntValue((Boolean) rightValue); truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForNumberComparison( actualIntValue, expectedIntValue, comparisonOperatorType); - } else if (actualValue instanceof List && expectedValue instanceof List) { - truthnessOfComparison = calculateTruthnessForListComparison((List) actualValue, (List) expectedValue, comparisonOperatorType); + } else if (leftValue instanceof List && rightValue instanceof List) { + truthnessOfComparison = compareNonNullLists((List) leftValue, comparisonOperatorType, (List) rightValue); - } else if (actualValue instanceof Date && expectedValue instanceof Date) { - truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForInstantComparison(convertToInstant(actualValue), convertToInstant(expectedValue), comparisonOperatorType); + } else if (leftValue instanceof Date && rightValue instanceof Date) { + truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForInstantComparison(convertToInstant(leftValue), convertToInstant(rightValue), comparisonOperatorType); - } else if (BsonHelper.isBsonTimestamp(actualValue) && BsonHelper.isBsonTimestamp(expectedValue)) { - long actualTimestamp = BsonHelper.getBsonTimestampValue(actualValue); - long expectedTimestamp = BsonHelper.getBsonTimestampValue(expectedValue); + } else if (BsonHelper.isBsonTimestamp(leftValue) && BsonHelper.isBsonTimestamp(rightValue)) { + long actualTimestamp = BsonHelper.getBsonTimestampValue(leftValue); + long expectedTimestamp = BsonHelper.getBsonTimestampValue(rightValue); truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForNumberComparison(actualTimestamp, expectedTimestamp, comparisonOperatorType); - } else if (BsonHelper.isObjectId(actualValue) || BsonHelper.isObjectId(expectedValue)) { - String actualString = actualValue.toString(); - String expectedString = expectedValue.toString(); + } else if (BsonHelper.isObjectId(leftValue) || BsonHelper.isObjectId(rightValue)) { + String actualString = leftValue.toString(); + String expectedString = rightValue.toString(); if (taintHandler != null && comparisonOperatorType == SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO) { taintHandler.handleTaintForStringEquals(actualString, expectedString, false); } @@ -303,7 +275,7 @@ private Truthness computeHeuristicComparisonNonNullValues(Object actualValue, Ob return truthnessOfComparison; } - private static Truthness computeHeuristicComparisonNumberValues(Number leftNumber, Number rightNumber, SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType) { + private static Truthness compareNumberValues(Number leftNumber, SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType, Number rightNumber) { Objects.requireNonNull(leftNumber); Objects.requireNonNull(rightNumber); Objects.requireNonNull(comparisonOperatorType); @@ -338,10 +310,6 @@ private static Truthness computeHeuristicComparisonNumberValues(Number leftNumbe } } - private static int toIntValue(Boolean actualValue) { - return actualValue ? 1 : 0; - } - /** * Checks if the provided value is of a supported type for comparison. * @@ -367,40 +335,30 @@ private static boolean isTypeSupportedForComparison(Object value) { * If the field "f" is present, null values are considered equal, and non-null values are compared * using the corresponding heuristic score for non-null values. * - * @param operation the {"f",{"$eq": value }} query - * @param document the BSON document to evaluate the heuristic score against + * @param operation the {"f",{"$eq": value }} query + * @param actualValue the value to evaluate the heuristic score against * @return */ - private Truthness computeHeuristic(EqualsOperation operation, Object document) { - requireNonNullQueryAndDocument(operation, document); - - String fieldName = operation.getFieldName(); + private Truthness evaluate(EqualsOperation operation, Object actualValue) { + Objects.requireNonNull(operation); Object expectedValue = operation.getValue(); - - Object actualValue; - if (documentContainsField(document, fieldName)) { - actualValue = getValue(document, fieldName); - } else { - actualValue = null; - } - if ((actualValue instanceof List) && !(expectedValue instanceof List)) { return computeHeuristicContainsElement(expectedValue, (List) actualValue); } else { - return computeHeuristicComparisonNullableValues( + return compareNullableValues( expectedValue, - actualValue, - SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO); + SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO, actualValue + ); } } - private Truthness computeHeuristicComparisonNullableValues(Object expectedValue, Object actualValue, SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType) { - if (expectedValue == null || actualValue == null) { + private Truthness compareNullableValues(Object leftValue, SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType, Object rightValue) { + if (leftValue == null || rightValue == null) { switch (comparisonOperatorType) { case EQUALS_TO: - return (expectedValue == null && actualValue == null) ? TRUE_C : C_FALSE; + return (leftValue == null && rightValue == null) ? TRUE_C : C_FALSE; case NOT_EQUALS_TO: - return (expectedValue == null && actualValue == null) ? C_FALSE : TRUE_C; + return (leftValue == null && rightValue == null) ? C_FALSE : TRUE_C; case GREATER_THAN: case GREATER_THAN_EQUALS: case MINOR_THAN: @@ -410,9 +368,9 @@ private Truthness computeHeuristicComparisonNullableValues(Object expectedValue, throw new IllegalArgumentException("Unsupported comparison operator type: " + comparisonOperatorType); } } else { - Truthness valTruthness = computeHeuristicComparisonNonNullValues(actualValue, - expectedValue, - comparisonOperatorType); + Truthness valTruthness = compareNonNullValues(rightValue, + comparisonOperatorType, leftValue + ); return buildSafeScaledTruthness(valTruthness); } } @@ -424,122 +382,95 @@ private Truthness computeHeuristicComparisonNullableValues(Object expectedValue, * to the expected value. If the condition is satisfied, the score is inverted to * reflect the distance from the condition being false. * - * @param operation the {"f",{"$ne": value}} query encapsulated as a NotEqualsOperation. - * This operation specifies the field name and the expected value - * for the inequality check. - * @param document the BSON document to evaluate the heuristic score against. - * The document may or may not contain the field to be checked. + * @param operation the {"f",{"$ne": value}} query encapsulated as a NotEqualsOperation. + * This operation specifies the field name and the expected value + * for the inequality check. + * @param actualValue the actual value in the document being evaluated. This value is + * obtained from the document and is compared against the expected value + * specified in the operation. * @return a Truthness object representing the distance of the document from meeting * the inequality condition, where one of the values (true or false) is 1, * and the other represents the distance to the alternate condition. */ - private Truthness computeHeuristic(NotEqualsOperation operation, Object document) { - requireNonNullQueryAndDocument(operation, document); + private Truthness evaluate(NotEqualsOperation operation, Object actualValue) { + Objects.requireNonNull(operation); Object expectedValue = operation.getValue(); - String fieldName = operation.getFieldName(); - - Object actualValue; - if (documentContainsField(document, fieldName)) { - actualValue = getValue(document, fieldName); - } else { - actualValue = null; - } if ((actualValue instanceof List) && !(expectedValue instanceof List)) { return computeHeuristicContainsElement(expectedValue, (List) actualValue).invert(); } else { - return computeHeuristicComparisonNullableValues( + return compareNullableValues( expectedValue, - actualValue, - SqlExpressionEvaluator.ComparisonOperatorType.NOT_EQUALS_TO); + SqlExpressionEvaluator.ComparisonOperatorType.NOT_EQUALS_TO, actualValue + ); } } - private Truthness computeHeuristic(GreaterThanOperation operation, Object document) { - requireNonNullQueryAndDocument(operation, document); + private Truthness evaluate(GreaterThanOperation operation, Object actualValue) { + Objects.requireNonNull(operation); Object expectedValue = operation.getValue(); - String fieldName = operation.getFieldName(); + return evaluateNested(actualValue, + value -> compareNullableValues( + expectedValue, + SqlExpressionEvaluator.ComparisonOperatorType.GREATER_THAN, value + )); - if (!documentContainsField(document, fieldName)) { - return C_FALSE; - } else { - Object actualValue = getValue(document, fieldName); - return computeHeuristicForActualValueOrAnyElement(actualValue, - value -> computeHeuristicComparisonNullableValues( - expectedValue, - value, - SqlExpressionEvaluator.ComparisonOperatorType.GREATER_THAN)); - - } } - private Truthness computeHeuristic(GreaterThanEqualsOperation operation, Object document) { - requireNonNullQueryAndDocument(operation, document); - - final String fieldName = operation.getFieldName(); + private Truthness evaluate(GreaterThanEqualsOperation operation, Object actualValue) { + Objects.requireNonNull(operation); final Object expectedValue = operation.getValue(); - if (!documentContainsField(document, fieldName)) { - return C_FALSE; - } else { - Object actualValue = getValue(document, fieldName); - return computeHeuristicForActualValueOrAnyElement(actualValue, - value -> computeHeuristicComparisonNullableValues( - expectedValue, - value, - SqlExpressionEvaluator.ComparisonOperatorType.GREATER_THAN_EQUALS)); + return evaluateNested(actualValue, + value -> compareNullableValues( + expectedValue, + SqlExpressionEvaluator.ComparisonOperatorType.GREATER_THAN_EQUALS, value + )); - } } - private Truthness computeHeuristic(LessThanOperation operation, Object document) { - requireNonNullQueryAndDocument(operation, document); - - final String fieldName = operation.getFieldName(); + private Truthness evaluate(LessThanOperation operation, Object actualValue) { + Objects.requireNonNull(operation); final Object expectedValue = operation.getValue(); - if (!documentContainsField(document, fieldName)) { - return C_FALSE; - } else { - Object actualValue = getValue(document, fieldName); - return computeHeuristicForActualValueOrAnyElement(actualValue, - value -> computeHeuristicComparisonNullableValues( - expectedValue, - value, - SqlExpressionEvaluator.ComparisonOperatorType.MINOR_THAN)); - } + return evaluateNested(actualValue, + value -> compareNullableValues( + expectedValue, + SqlExpressionEvaluator.ComparisonOperatorType.MINOR_THAN, value + )); } - private Truthness computeHeuristic(LessThanEqualsOperation operation, Object document) { - requireNonNullQueryAndDocument(operation, document); + private Truthness evaluate(LessThanEqualsOperation operation, Object actualValue) { + Objects.requireNonNull(operation); - final String fieldName = operation.getFieldName(); final Object expectedValue = operation.getValue(); - - if (!documentContainsField(document, fieldName)) { - return C_FALSE; - } else { - Object actualValue = getValue(document, fieldName); - return computeHeuristicForActualValueOrAnyElement(actualValue, - value -> computeHeuristicComparisonNullableValues( - expectedValue, - value, - SqlExpressionEvaluator.ComparisonOperatorType.MINOR_THAN_EQUALS)); - - } + return evaluateNested(actualValue, + value -> compareNullableValues( + expectedValue, + SqlExpressionEvaluator.ComparisonOperatorType.MINOR_THAN_EQUALS, value + )); } - private Truthness computeHeuristicForActualValueOrAnyElement(Object actualValue, - Function elementHeuristic) { + /** + * Evaluates a nested structure or a single value using a provided heuristic function. + * For nested structures that are lists, it aggregates the results of applying the heuristic + * function to each element in the list. For single values, it directly applies the heuristic. + * + * @param value the value to evaluate, which can either be a single value or a list of values + * @param elementHeuristic a function to compute the heuristic for each element or the single value + * @return a Truthness object representing the heuristic of the evaluated input + */ + private Truthness evaluateNested(Object value, + Function elementHeuristic) { Objects.requireNonNull(elementHeuristic); - if (!(actualValue instanceof List)) { - return elementHeuristic.apply(actualValue); + if (!(value instanceof List)) { + return elementHeuristic.apply(value); } - List values = (List) actualValue; + List values = (List) value; if (values.isEmpty()) { return C_FALSE; } @@ -551,10 +482,10 @@ private Truthness computeHeuristicForActualValueOrAnyElement(Object actualValue, } private Truthness computeHeuristic(OrOperation operation, Object document) { - requireNonNullQueryAndDocument(operation, document); + Objects.requireNonNull(operation); Truthness[] results = operation.getConditions().stream() - .map(condition -> computeHeuristicOnDocument(condition, document)) + .map(condition -> computeHeuristicQueryOperation(condition, document)) .toArray(Truthness[]::new); return buildOrAggregationTruthness(results); } @@ -563,28 +494,16 @@ private Truthness computeHeuristic(AndOperation operation, Object document) { requireNonNullQueryAndDocument(operation, document); Truthness[] results = operation.getConditions().stream() - .map(condition -> computeHeuristicOnDocument(condition, document)) + .map(condition -> computeHeuristicQueryOperation(condition, document)) .toArray(Truthness[]::new); return buildAndAggregationTruthness(results); } - private Truthness computeHeuristic(InOperation operation, Object document) { - requireNonNullQueryAndDocument(operation, document); + private Truthness evaluate(InOperation operation, Object actualValue) { + Objects.requireNonNull(operation); List expectedValueList = operation.getValues(); - final String fieldName = operation.getFieldName(); - - final Object actualValue; - if (documentContainsField(document, fieldName)) { - actualValue = getValue(document, fieldName); - } else { - // If the document does not have a field - // with that name, we consider the field - // value to be null - actualValue = null; - } - final Truthness res = computeHeuristicInOperation(actualValue, expectedValueList); return res; } @@ -605,27 +524,18 @@ private Truthness computeHeuristicContainsElement(Object element, List list) return C_FALSE; } else { Truthness res = buildOrAggregationTruthness(list.stream() - .map(expectedValue -> computeHeuristicComparisonNullableValues(expectedValue, - element, - SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO)) + .map(expectedValue -> compareNullableValues(expectedValue, + SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO, element + )) .toArray(Truthness[]::new)); return buildSafeScaledTruthness(res); } } - private Truthness computeHeuristic(NotInOperation operation, Object document) { - requireNonNullQueryAndDocument(operation, document); + private Truthness evaluate(NotInOperation operation, Object actualValue) { + Objects.requireNonNull(operation); List expectedValueList = operation.getValues(); - final String fieldName = operation.getFieldName(); - - final Object actualValue; - if (!documentContainsField(document, fieldName)) { - actualValue = null; - } else { - actualValue = getValue(document, fieldName); - } - final Truthness res = computeHeuristicInOperation(actualValue, expectedValueList); return res.invert(); } @@ -667,37 +577,33 @@ private Truthness computeHeuristicInOperation(Object actualValue, List expect return res; } - private Truthness computeHeuristic(AllOperation operation, Object document) { - requireNonNullQueryAndDocument(operation, document); + private Truthness evaluate(AllOperation operation, Object actualValue) { + Objects.requireNonNull(operation); final List expectedValues = operation.getValues(); - final String fieldName = operation.getFieldName(); if (expectedValues.isEmpty()) { return C_FALSE; } - if (!documentContainsField(document, fieldName)) { - return isSingleNullExpectedValue(expectedValues) ? TRUE_C : C_FALSE; - } - - final Object actualFieldValue = getValue(document, fieldName); - if (actualFieldValue == null) { - return isSingleNullExpectedValue(expectedValues) ? TRUE_C : C_FALSE; + if (actualValue == null) { + return (expectedValues.size() == 1 && expectedValues.get(0) == null) + ? TRUE_C + : C_FALSE; } - if (!(actualFieldValue instanceof List)) { + if (!(actualValue instanceof List)) { if (expectedValues.size() != 1) { return C_FALSE; } else { - return computeHeuristicComparisonNullableValues( + return compareNullableValues( expectedValues.get(0), - actualFieldValue, - SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO); + SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO, actualValue + ); } } - List actualValues = (List) actualFieldValue; + List actualValues = (List) actualValue; if (actualValues.isEmpty()) { return C_FALSE; } @@ -708,91 +614,80 @@ private Truthness computeHeuristic(AllOperation operation, Object document) { return buildSafeScaledTruthness(res); } - private static boolean isSingleNullExpectedValue(List expectedValues) { - return expectedValues.size() == 1 && expectedValues.get(0) == null; - } - - private static Truthness buildSafeScaledTruthness(Truthness truthness) { - Objects.requireNonNull(truthness); - - return buildSafeScaledTruthness(truthness.getOfTrue()); - } - - - private Truthness computeHeuristic(SizeOperation operation, Object document) { - requireNonNullQueryAndDocument(operation, document); - Objects.requireNonNull(operation.getValue()); + private Truthness evaluate(SizeOperation operation, Object actualValue) { + Objects.requireNonNull(operation); - if (!documentContainsField(document, operation.getFieldName())) { + if (actualValue == null || !(actualValue instanceof List)) { return C_FALSE; } else { - Object actualValue = getValue(document, operation.getFieldName()); - if (actualValue == null || !(actualValue instanceof List)) { - return C_FALSE; - } else { - int actualSize = ((List) actualValue).size(); - int expectedSize = operation.getValue().intValue(); - Truthness res = getEqualityTruthness(actualSize, expectedSize); - return buildSafeScaledTruthness(res); - } + int actualSize = ((List) actualValue).size(); + int expectedSize = operation.getValue().intValue(); + Truthness res = getEqualityTruthness(actualSize, expectedSize); + return buildSafeScaledTruthness(res); } } - private Truthness computeHeuristic(ElemMatchOperation operation, Object document) { - requireNonNullQueryAndDocument(operation, document); - Objects.requireNonNull(operation.getFieldName()); - Objects.requireNonNull(operation.getCondition()); + private Truthness evaluate(ElemMatchOperation operation, Object actualValue) { + Objects.requireNonNull(operation); - if (!documentContainsField(document, operation.getFieldName())) { + if (actualValue == null || !(actualValue instanceof List)) { return C_FALSE; } else { - Object actualValue = getValue(document, operation.getFieldName()); - if (actualValue == null || !(actualValue instanceof List)) { + List actualList = (List) actualValue; + if (actualList.isEmpty()) { return C_FALSE; } else { - List actualList = (List) actualValue; - if (actualList.isEmpty()) { - return C_FALSE; - } else { - Truthness orAggregation = buildOrAggregationTruthness(actualList.stream() - .map(listElement -> computeHeuristicOnElemMatchElement( - operation.getCondition(), listElement, document)) - .toArray(Truthness[]::new)); - return buildSafeScaledTruthness(orAggregation); - } + Truthness orAggregation = buildOrAggregationTruthness(actualList.stream() + .map(listElement -> computeHeuristicOnElement( + operation.getCondition(), listElement)) + .toArray(Truthness[]::new)); + return buildSafeScaledTruthness(orAggregation); } } } - private Truthness computeHeuristicOnElemMatchElement(QueryOperation condition, Object element, Object documentTemplate) { - if (isBsonDocument(element)) { - return computeHeuristicOnDocument(condition, element); + private Truthness computeHeuristicOnElement(QueryOperation condition, Object element) { + if (condition instanceof AndOperation) { + return buildAndAggregationTruthness(((AndOperation) condition).getConditions().stream() + .map(child -> computeHeuristicOnElement(child, element)) + .toArray(Truthness[]::new)); + } else if (condition instanceof OrOperation) { + return buildOrAggregationTruthness(((OrOperation) condition).getConditions().stream() + .map(child -> computeHeuristicOnElement(child, element)) + .toArray(Truthness[]::new)); + } else if (condition instanceof QueryOperationWithField + && "$".equals(((QueryOperationWithField) condition).getFieldName())) { + // The parser uses a synthetic field for operators applied to the element itself. + return evaluate(condition, element); } - if (!(condition instanceof QueryOperationWithField)) { - return C_FALSE; + if (isBsonDocument(element)) { + return computeHeuristicQueryOperation(condition, element); } - Object elementDocument = newDocument(documentTemplate); - appendToDocument(elementDocument, ((QueryOperationWithField) condition).getFieldName(), element); - return computeHeuristicOnDocument(condition, elementDocument); + return C_FALSE; } - private Truthness computeHeuristic(ExistsOperation operation, Object document) { - requireNonNullQueryAndDocument(operation, document); + private Truthness computeHeuristic(ExistsOperation operation, Object input) { + Objects.requireNonNull(operation); + + if (!isBsonDocument(input)) { + // If the input is not a BSON document, the existence of a field is always false. + return C_FALSE; + } String expectedFieldName = operation.getFieldName(); - Set actualFieldNames = documentKeys(document); + Set actualFieldNames = documentKeys(input); final Truthness res; if (actualFieldNames.isEmpty()) { res = C_FALSE; } else { Truthness orTruthness = buildOrAggregationTruthness(actualFieldNames.stream() .map(actualFieldName -> - computeHeuristicComparisonNonNullValues(actualFieldName, - expectedFieldName, - SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO)) + compareNonNullValues(actualFieldName, + SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO, expectedFieldName + )) .toArray(Truthness[]::new)); res = buildSafeScaledTruthness(orTruthness); } @@ -814,23 +709,12 @@ private static void requireNonNullQueryAndDocument(QueryOperation operation, Obj } } - private Truthness computeHeuristic(ModOperation operation, Object document) { - requireNonNullQueryAndDocument(operation, document); - Objects.requireNonNull(operation.getDivisor()); - Objects.requireNonNull(operation.getRemainder()); + private Truthness evaluate(ModOperation operation, Object actualValue) { + Objects.requireNonNull(operation); long divisor = operation.getDivisor().longValue(); long expectedRemainder = operation.getRemainder().longValue(); - - final String fieldName = operation.getFieldName(); - final Object actualValue; - if (!documentContainsField(document, fieldName)) { - actualValue = null; - } else { - actualValue = getValue(document, fieldName); - } - - return computeHeuristicForActualValueOrAnyElement(actualValue, + return evaluateNested(actualValue, value -> computeHeuristicModOnSingleValue(value, divisor, expectedRemainder)); } @@ -844,20 +728,16 @@ private Truthness computeHeuristicModOnSingleValue(Object value, long divisor, l return buildSafeScaledTruthness(res); } - private Truthness computeHeuristic(BitsOperation operation, Object document) { - requireNonNullQueryAndDocument(operation, document); - - String fieldName = operation.getFieldName(); - if (!documentContainsField(document, fieldName)) { - return C_FALSE; - } - - Object actualValue = getValue(document, fieldName); - return computeHeuristicForActualValueOrAnyElement(actualValue, - value -> computeHeuristicBitsOnSingleValue(value, operation)); + private Truthness evaluate(BitsOperation operation, Object actualValue) { + Objects.requireNonNull(operation); + return evaluateNested(actualValue, + value -> evaluateOnSingleValue(operation, value)); } - private Truthness computeHeuristicBitsOnSingleValue(Object value, BitsOperation operation) { + private Truthness evaluateOnSingleValue(BitsOperation operation, + Object value) { + Objects.requireNonNull(operation); + if (!(value instanceof Number)) { return C_FALSE; } @@ -888,24 +768,30 @@ private Truthness computeHeuristicBitsOnSingleValue(Object value, BitsOperation } } - private Truthness computeHeuristic(NotOperation operation, Object document) { - requireNonNullQueryAndDocument(operation, document); + private Truthness computeHeuristicOnValue(NotOperation operation, Object actualValue) { + Objects.requireNonNull(operation); + QueryOperation condition = operation.getCondition(); - return computeHeuristicOnDocument(condition, document).invert(); + Truthness conditionTruthness = computeHeuristicOnElement(condition, actualValue); + return conditionTruthness.invert(); } private Truthness computeHeuristic(NorOperation operation, Object document) { - requireNonNullQueryAndDocument(operation, document); + Objects.requireNonNull(operation); Truthness orRes = buildOrAggregationTruthness(operation.getConditions() .stream() - .map(condition -> computeHeuristicOnDocument(condition, document)) + .map(condition -> computeHeuristicQueryOperation(condition, document)) .toArray(Truthness[]::new)); return orRes.invert(); } private Truthness computeHeuristic(TypeOperation operation, Object document) { - requireNonNullQueryAndDocument(operation, document); + Objects.requireNonNull(operation); + + if (!isBsonDocument(document)) { + return C_FALSE; + } String fieldName = operation.getFieldName(); if (!documentContainsField(document, fieldName)) { @@ -922,26 +808,21 @@ private Truthness computeHeuristic(TypeOperation operation, Object document) { } } - private Truthness computeHeuristic(NearSphereOperation operation, Object document) { + private Truthness evaluate(NearSphereOperation operation, Object actualValue) { Objects.requireNonNull(operation); - Objects.requireNonNull(document); - Objects.requireNonNull(operation.getFieldName()); - - final String fieldName = operation.getFieldName(); - final Object fieldValue = getValue(document, fieldName); final double longitude = operation.getLongitude(); final double latitude = operation.getLatitude(); - return computeHeuristic(operation, longitude, latitude, fieldValue, SPHERICAL); + return evaluate(operation, longitude, latitude, actualValue, SPHERICAL); } - private static Truthness computeHeuristic(AbstractProximityOperation abstractProximityOperation, - double longitude, - double latitude, - Object fieldValue, - GeoSpatialModel geoSpatialModel) { + private static Truthness evaluate(AbstractProximityOperation abstractProximityOperation, + double longitude, + double latitude, + Object actualValue, + GeoSpatialModel geoSpatialModel) { Objects.requireNonNull(abstractProximityOperation); double x1 = geoSpatialModel == SPHERICAL ? Math.toRadians(longitude) : longitude; @@ -954,9 +835,9 @@ private static Truthness computeHeuristic(AbstractProximityOperation abstractPro type key is case-sensitive. (https://datatracker.ietf.org/doc/html/rfc7946#section-1.4) */ - if (isBsonDocument(fieldValue) - && GeoJsonUtils.isGeoJsonPoint(fieldValue)) { - GeoJsonPoint geoJsonPoint = GeoJsonUtils.toGeoJsonPoint(fieldValue); + if (isBsonDocument(actualValue) + && GeoJsonUtils.isGeoJsonPoint(actualValue)) { + GeoJsonPoint geoJsonPoint = GeoJsonUtils.toGeoJsonPoint(actualValue); x2 = geoSpatialModel == SPHERICAL ? Math.toRadians(geoJsonPoint.getLongitude()) : geoJsonPoint.getLongitude(); @@ -985,11 +866,11 @@ private static Truthness computeHeuristic(AbstractProximityOperation abstractPro : getEqualityTruthness(distanceBetweenPoints, min); } - private Truthness calculateTruthnessForListComparison(List actualList, List expectedList, SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType) { - Objects.requireNonNull(actualList); - Objects.requireNonNull(expectedList); + private Truthness compareNonNullLists(List leftList, SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType, List rightList) { + Objects.requireNonNull(leftList); + Objects.requireNonNull(rightList); - final Truthness truthness = computeHeuristicListEquality(actualList, expectedList); + final Truthness truthness = computeHeuristicListEquality(leftList, rightList); switch (comparisonOperatorType) { case EQUALS_TO: return truthness; @@ -1012,10 +893,10 @@ private Truthness computeHeuristicListEquality(List actualList, List expec Truthness[] arrayOfTruthnesses = new Truthness[actualList.size()]; for (int i = 0; i < actualList.size(); i++) { - arrayOfTruthnesses[i] = computeHeuristicComparisonNullableValues( + arrayOfTruthnesses[i] = compareNullableValues( actualList.get(i), - expectedList.get(i), - SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO); + SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO, expectedList.get(i) + ); } Truthness unscaledTruthness = buildAndAggregationTruthness(arrayOfTruthnesses); final Truthness truthness = buildSafeScaledTruthness(unscaledTruthness); diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java new file mode 100644 index 0000000000..eaf4c1322b --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java @@ -0,0 +1,79 @@ +package org.evomaster.client.java.controller.mongo; + +import org.evomaster.client.java.distance.heuristics.Truthness; +import org.evomaster.client.java.instrumentation.coverage.methodreplacement.RegexDistanceUtils; +import org.evomaster.client.java.sql.internal.TaintHandler; + +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.evomaster.client.java.distance.heuristics.TruthnessUtils.buildScaledTruthness; + +public abstract class MongoHeuristicsCalculatorHelper { + + // TODO these constants should be replaced by DistanceHelper constants + public static final double C = 0.1; + public static final Truthness C_FALSE = new Truthness(C, 1.0); + // TODO These constants should be refactored by TruthnessUtils constants + public static final Truthness TRUE_C = new Truthness(1.0, C); + + static Truthness buildSafeScaledTruthness(double maxOfTrue) { + if (maxOfTrue == 1.0) { + return TRUE_C; + } else { + return buildScaledTruthness(C, maxOfTrue); + } + } + + /** + * Evaluates whether the given input string matches the provided regular expression pattern. + * The result is represented as a {@link Truthness} object, which captures both + * the definitive match result and the closeness to a potential match. + * Optionally, a {@link TaintHandler} can be used to handle tainting information + * related to the regular expression processing. + * + * @param inputValue the input string to be matched against the regular expression + * @param pattern the compiled {@link Pattern} representing the regular expression to match + * @param taintHandler an optional implementation of {@link TaintHandler} to handle taint-related processing; + * may be null if taint handling is not required + * @return a {@link Truthness} object representing the result of the evaluation, + * where one component (true or false) is 1 to reflect the match status, and the other + * captures the approximation in cases of non-exact matches + */ + static Truthness evaluateRegularExpression(String inputValue, Pattern pattern, TaintHandler taintHandler) { + final String patternString = pattern.pattern(); + + if (taintHandler != null) { + final int patternFlags = pattern.flags(); + // TODO: tainting should take into account the pattern flags, which can change the matching behavior + // TODO: regex could be a partial word match (MongoDB $regex) instead of a whole word match (Matcher.matches()) + taintHandler.handleTaintForRegex(inputValue, patternString); + } + + + Matcher matcher = pattern.matcher(inputValue); + boolean matches = matcher.find(); + + if (matches) { + return TRUE_C; + } else { + // TODO this does not take into account pattern flags, which can change the matching behavior + final int distance = RegexDistanceUtils.getStandardDistance(inputValue, patternString); + // The distance approximation can be zero even when Java's matcher rejects the input + // (for example, when flags affect line terminators). Keep non-matches strictly false. + double ofTrue = 1d / (1.1d + distance); + return buildSafeScaledTruthness(ofTrue); + } + } + + static int toIntValue(Boolean actualValue) { + return actualValue ? 1 : 0; + } + + static Truthness buildSafeScaledTruthness(Truthness truthness) { + Objects.requireNonNull(truthness); + + return buildSafeScaledTruthness(truthness.getOfTrue()); + } +} diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 27ec65ff8b..3efc888435 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -481,6 +481,44 @@ public void testElemMatchMissingField() { assertTrue(distanceMatch.isFalse()); } + @Test + public void testElemMatchScalarConditionsMustMatchSameElement() { + Document query = new Document("values", + new Document("$elemMatch", new Document("$gt", 2).append("$lt", 5))); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + assertTrue(calculator.computeHeuristicDocument(query, + new Document("values", Arrays.asList(1, 3, 6))).isTrue()); + assertTrue(calculator.computeHeuristicDocument(query, + new Document("values", Arrays.asList(1, 6))).isFalse()); + } + + @Test + public void testNotInsideElemMatchNegatesEachElement() { + Document condition = new Document("$not", new Document("$gt", 2)); + Document query = new Document("values", new Document("$elemMatch", condition)); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + Document mixed = new Document("values", Arrays.asList(1, 6)); + + assertTrue(calculator.computeHeuristicDocument(query, mixed).isTrue()); + assertTrue(calculator.computeHeuristicDocument(query, + new Document("values", Arrays.asList(3, 6))).isFalse()); + assertTrue(calculator.computeHeuristicDocument(new Document("values", condition), mixed).isFalse()); + assertTrue(calculator.computeHeuristicDocument(new Document("values", condition), + new Document("values", Arrays.asList(1, 2))).isTrue()); + } + + @Test + public void testElemMatchDocumentConditionOnMixedArray() { + Document query = new Document("values", new Document("$elemMatch", new Document("x", null))); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + assertTrue(calculator.computeHeuristicDocument(query, + new Document("values", Arrays.asList(null, 1, "abc", new Document("x", 2)))).isFalse()); + assertTrue(calculator.computeHeuristicDocument(query, + new Document("values", Arrays.asList(null, 1, new Document("x", null)))).isTrue()); + } + @Test public void testElemMatchNotAList() { Document doc = new Document().append("results", new Document("product", "abc")); @@ -626,6 +664,16 @@ public void testNotNullValue() { assertTrue(distanceMatch.isFalse()); } + @Test + public void testNotExistsPreservesMissingAndNullFields() { + Document query = convertToDocument(Filters.not(Filters.exists("age"))); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + assertTrue(calculator.computeHeuristicDocument(query, new Document()).isTrue()); + assertTrue(calculator.computeHeuristicDocument(query, new Document("age", null)).isFalse()); + assertTrue(calculator.computeHeuristicDocument(query, new Document("age", 20)).isFalse()); + } + @Test public void testExistsTrueValue() { Document doc = new Document().append("age", 20); @@ -1879,7 +1927,6 @@ public void testBitsDoesNotMatchANonIntegralNumber() { } @Test - @Disabled("$elemMatch with a condition on a field matches an array of scalars") public void testElemMatchOnAnArrayOfScalars() { // mongo: no match, no element of the array is a document holding "x" Document doc = new Document().append("a", new ArrayList<>(Arrays.asList(1, 2, 3))); @@ -2059,10 +2106,12 @@ public void testNotWithMoreThanOneInnerOperator() { Document twoOperators = new Document().append("a", new Document().append("$not", new Document().append("$gt", 1).append("$lt", 9))); assertTrue(calculator.computeHeuristicDocument(twoOperators, doc).isTrue()); + assertTrue(calculator.computeHeuristicDocument(twoOperators, new Document("a", 5)).isFalse()); // mongo: no match, the two negations cancel and 1 is not greater than 1 Document nested = new Document().append("a", new Document().append("$not", new Document().append("$not", new Document().append("$gt", 1)))); assertTrue(calculator.computeHeuristicDocument(nested, doc).isFalse()); + assertTrue(calculator.computeHeuristicDocument(nested, new Document("a", 5)).isTrue()); } } From eae56b1d97067441c9c6a114c821346b5601df9f Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Fri, 11 Sep 2026 15:57:17 -0300 Subject: [PATCH 18/53] Refactor Mongo heuristic calculations: extract helper class, simplify logic, and improve modularity for comparison and bitwise operations --- .../mongo/MongoHeuristicsCalculator.java | 462 ++++-------------- 1 file changed, 101 insertions(+), 361 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java index 5d4082286b..52edfcbc1e 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java @@ -1,10 +1,7 @@ package org.evomaster.client.java.controller.mongo; import org.evomaster.client.java.controller.internal.db.mongo.MongoDistanceWithMetrics; -import org.evomaster.client.java.controller.mongo.geometry.GeoJsonPoint; -import org.evomaster.client.java.controller.mongo.geometry.GeoJsonUtils; import org.evomaster.client.java.controller.mongo.operations.*; -import org.evomaster.client.java.controller.mongo.utils.BsonHelper; import org.evomaster.client.java.distance.heuristics.Truthness; import org.evomaster.client.java.sql.heuristic.SqlExpressionEvaluator; import org.evomaster.client.java.sql.internal.TaintHandler; @@ -14,24 +11,45 @@ import static org.evomaster.client.java.controller.mongo.utils.MongoUtils.*; import static org.evomaster.client.java.controller.mongo.utils.MongoUtils.GeoSpatialModel.SPHERICAL; import static org.evomaster.client.java.distance.heuristics.TruthnessUtils.*; -import static org.evomaster.client.java.sql.heuristic.ConversionHelper.convertToInstant; import java.util.*; import java.util.function.Function; import java.util.regex.Pattern; import java.util.stream.StreamSupport; +/** + * The MongoHeuristicsCalculator class provides methods to compute heuristic scores for MongoDB-like + * query operations against a set of documents. These heuristics aim to measure how closely a + * document satisfies the given query conditions. The class supports a variety of query operators, + * including equality, inequality, comparison, and logical operations. + * + */ public class MongoHeuristicsCalculator { + /** + * A handler responsible for managing taint propagation and tracking + * during the execution of heuristic calculations. This object is used + * to process and handle specific taint-related operations, such as + * string equality comparisons and regular expression evaluations. + */ private final TaintHandler taintHandler; + /** + * A helper component for handling the internal heuristic computation logic + * within the context of Mongo operations. It provides utilities and lower-level + * methods that are invoked by the main computation flow in + * {@code MongoHeuristicsCalculator}. + */ + private final MongoHeuristicsCalculatorHelper helper; + public MongoHeuristicsCalculator() { this(null); } public MongoHeuristicsCalculator(TaintHandler taintHandler) { this.taintHandler = taintHandler; + this.helper = new MongoHeuristicsCalculatorHelper(taintHandler); } @@ -153,10 +171,16 @@ private Truthness evaluate(QueryOperation operation, Object actualValue) { return evaluate((SizeOperation) operation, actualValue); } else if (operation instanceof ModOperation) { return evaluate((ModOperation) operation, actualValue); - } else if (operation instanceof BitsOperation) { - return evaluate((BitsOperation) operation, actualValue); + } else if (operation instanceof BitsAllClearOperation) { + return evaluate((BitsAllClearOperation) operation, actualValue); + } else if (operation instanceof BitsAllSetOperation) { + return evaluate((BitsAllSetOperation) operation, actualValue); + } else if (operation instanceof BitsAnyClearOperation) { + return evaluate((BitsAnyClearOperation) operation, actualValue); + } else if (operation instanceof BitsAnySetOperation) { + return evaluate((BitsAnySetOperation) operation, actualValue); } else if (operation instanceof NotOperation) { - return computeHeuristicOnValue((NotOperation) operation, actualValue); + return evaluate((NotOperation) operation, actualValue); } else if (operation instanceof RegexOperation) { return evaluate((RegexOperation) operation, actualValue); } else if (operation instanceof NearSphereOperation) { @@ -206,7 +230,15 @@ private Truthness evaluate(NearOperation operation, Object actualValue) { GeoSpatialModel model = operation.hasLegacyCoordinates() ? GeoSpatialModel.PLANAR : SPHERICAL; - return evaluate(operation, longitude, latitude, actualValue, model); + + final double minDistance = operation.hasMinDistance() ? operation.getMinDistance() : 0.0; + final double maxDistance = operation.hasMaxDistance() ? operation.getMaxDistance() : Double.MAX_VALUE; + + return helper.evaluate( + minDistance, maxDistance, + longitude, latitude, + actualValue, + model); } /** @@ -222,111 +254,6 @@ private Truthness computeHeuristic(EmptyOperation operation, Object document) { return TRUE_C; } - private Truthness compareNonNullValues(Object leftValue, SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType, Object rightValue) { - Objects.requireNonNull(leftValue); - Objects.requireNonNull(rightValue); - - final Truthness truthnessOfComparison; - if (leftValue instanceof Number && rightValue instanceof Number) { - final Number actualNumber = (Number) leftValue; - final Number expectedNumber = (Number) rightValue; - truthnessOfComparison = compareNumberValues(actualNumber, comparisonOperatorType, expectedNumber); - - } else if (leftValue instanceof String && rightValue instanceof String) { - String actualString = (String) leftValue; - String expectedString = (String) rightValue; - if (taintHandler != null && comparisonOperatorType == SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO) { - taintHandler.handleTaintForStringEquals(actualString, expectedString, false); - } - truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForStringComparison(actualString, expectedString, comparisonOperatorType); - - } else if (leftValue instanceof Boolean && rightValue instanceof Boolean) { - int actualIntValue = toIntValue((Boolean) leftValue); - int expectedIntValue = toIntValue((Boolean) rightValue); - truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForNumberComparison( - actualIntValue, expectedIntValue, comparisonOperatorType); - - } else if (leftValue instanceof List && rightValue instanceof List) { - truthnessOfComparison = compareNonNullLists((List) leftValue, comparisonOperatorType, (List) rightValue); - - } else if (leftValue instanceof Date && rightValue instanceof Date) { - truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForInstantComparison(convertToInstant(leftValue), convertToInstant(rightValue), comparisonOperatorType); - - - } else if (BsonHelper.isBsonTimestamp(leftValue) && BsonHelper.isBsonTimestamp(rightValue)) { - long actualTimestamp = BsonHelper.getBsonTimestampValue(leftValue); - long expectedTimestamp = BsonHelper.getBsonTimestampValue(rightValue); - truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForNumberComparison(actualTimestamp, expectedTimestamp, comparisonOperatorType); - - } else if (BsonHelper.isObjectId(leftValue) || BsonHelper.isObjectId(rightValue)) { - String actualString = leftValue.toString(); - String expectedString = rightValue.toString(); - if (taintHandler != null && comparisonOperatorType == SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO) { - taintHandler.handleTaintForStringEquals(actualString, expectedString, false); - } - truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForStringComparison(actualString, expectedString, comparisonOperatorType); - - } else { - // If both types are supported, but no actual comparison logic is defined, - // we considered them to be incompatible, therefore the comparison returns true - // only if the comparison operator is NOT_EQUALS_TO. Otherwise returns false. - truthnessOfComparison = comparisonOperatorType == SqlExpressionEvaluator.ComparisonOperatorType.NOT_EQUALS_TO ? TRUE_C : C_FALSE; - } - return truthnessOfComparison; - } - - private static Truthness compareNumberValues(Number leftNumber, SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType, Number rightNumber) { - Objects.requireNonNull(leftNumber); - Objects.requireNonNull(rightNumber); - Objects.requireNonNull(comparisonOperatorType); - - double leftValueAsDouble = leftNumber.doubleValue(); - double rightValueAsDouble = rightNumber.doubleValue(); - - if (Double.isNaN(leftValueAsDouble) || Double.isNaN(rightValueAsDouble)) { - // handle case when NaN is involved in the comparison - switch (comparisonOperatorType) { - case EQUALS_TO: { - return (Double.isNaN(leftValueAsDouble) && Double.isNaN(rightValueAsDouble)) ? - TRUE_C : C_FALSE; - } - case NOT_EQUALS_TO: { - return (!Double.isNaN(leftValueAsDouble) || !Double.isNaN(rightValueAsDouble)) ? - TRUE_C : C_FALSE; - } - case GREATER_THAN: - case GREATER_THAN_EQUALS: - case MINOR_THAN: - case MINOR_THAN_EQUALS: { - return C_FALSE; - } - default: - throw new IllegalArgumentException("Unsupported comparison operator type: " + comparisonOperatorType); - } - } else { - // if both values are not NaN, we can use the standard comparison logic - final Truthness truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForNumberComparison(leftNumber, rightNumber, comparisonOperatorType); - return truthnessOfComparison; - } - } - - /** - * Checks if the provided value is of a supported type for comparison. - * - * @param value - * @return - */ - private static boolean isTypeSupportedForComparison(Object value) { - Objects.requireNonNull(value); - - return value instanceof String || - value instanceof Number || - value instanceof Boolean || - value instanceof Date || - value instanceof List || - BsonHelper.isObjectId(value) || - BsonHelper.isBsonTimestamp(value); - } /** * Computes the heuristic score for a {"f",{"$eq": value }} query. @@ -343,38 +270,15 @@ private Truthness evaluate(EqualsOperation operation, Object actualValue) { Objects.requireNonNull(operation); Object expectedValue = operation.getValue(); if ((actualValue instanceof List) && !(expectedValue instanceof List)) { - return computeHeuristicContainsElement(expectedValue, (List) actualValue); + return helper.computeHeuristicContainsElement(expectedValue, (List) actualValue); } else { - return compareNullableValues( + return helper.compareNullableValues( expectedValue, SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO, actualValue ); } } - private Truthness compareNullableValues(Object leftValue, SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType, Object rightValue) { - if (leftValue == null || rightValue == null) { - switch (comparisonOperatorType) { - case EQUALS_TO: - return (leftValue == null && rightValue == null) ? TRUE_C : C_FALSE; - case NOT_EQUALS_TO: - return (leftValue == null && rightValue == null) ? C_FALSE : TRUE_C; - case GREATER_THAN: - case GREATER_THAN_EQUALS: - case MINOR_THAN: - case MINOR_THAN_EQUALS: - return C_FALSE; - default: - throw new IllegalArgumentException("Unsupported comparison operator type: " + comparisonOperatorType); - } - } else { - Truthness valTruthness = compareNonNullValues(rightValue, - comparisonOperatorType, leftValue - ); - return buildSafeScaledTruthness(valTruthness); - } - } - /** * Computes the heuristic score for a {"f",{"$ne": value}} query. @@ -397,9 +301,9 @@ private Truthness evaluate(NotEqualsOperation operation, Object actualValue) Object expectedValue = operation.getValue(); if ((actualValue instanceof List) && !(expectedValue instanceof List)) { - return computeHeuristicContainsElement(expectedValue, (List) actualValue).invert(); + return helper.computeHeuristicContainsElement(expectedValue, (List) actualValue).invert(); } else { - return compareNullableValues( + return helper.compareNullableValues( expectedValue, SqlExpressionEvaluator.ComparisonOperatorType.NOT_EQUALS_TO, actualValue ); @@ -411,8 +315,8 @@ private Truthness evaluate(GreaterThanOperation operation, Object actualValue Objects.requireNonNull(operation); Object expectedValue = operation.getValue(); - return evaluateNested(actualValue, - value -> compareNullableValues( + return evaluateWithArrayUnwrapping(actualValue, + value -> helper.compareNullableValues( expectedValue, SqlExpressionEvaluator.ComparisonOperatorType.GREATER_THAN, value )); @@ -423,8 +327,8 @@ private Truthness evaluate(GreaterThanEqualsOperation operation, Object actua Objects.requireNonNull(operation); final Object expectedValue = operation.getValue(); - return evaluateNested(actualValue, - value -> compareNullableValues( + return evaluateWithArrayUnwrapping(actualValue, + value -> helper.compareNullableValues( expectedValue, SqlExpressionEvaluator.ComparisonOperatorType.GREATER_THAN_EQUALS, value )); @@ -435,8 +339,8 @@ private Truthness evaluate(LessThanOperation operation, Object actualValue) { Objects.requireNonNull(operation); final Object expectedValue = operation.getValue(); - return evaluateNested(actualValue, - value -> compareNullableValues( + return evaluateWithArrayUnwrapping(actualValue, + value -> helper.compareNullableValues( expectedValue, SqlExpressionEvaluator.ComparisonOperatorType.MINOR_THAN, value )); @@ -446,8 +350,8 @@ private Truthness evaluate(LessThanEqualsOperation operation, Object actualVa Objects.requireNonNull(operation); final Object expectedValue = operation.getValue(); - return evaluateNested(actualValue, - value -> compareNullableValues( + return evaluateWithArrayUnwrapping(actualValue, + value -> helper.compareNullableValues( expectedValue, SqlExpressionEvaluator.ComparisonOperatorType.MINOR_THAN_EQUALS, value )); @@ -458,27 +362,28 @@ private Truthness evaluate(LessThanEqualsOperation operation, Object actualVa * For nested structures that are lists, it aggregates the results of applying the heuristic * function to each element in the list. For single values, it directly applies the heuristic. * - * @param value the value to evaluate, which can either be a single value or a list of values + * @param value the value to evaluate, which can either be a single value or a list of values * @param elementHeuristic a function to compute the heuristic for each element or the single value * @return a Truthness object representing the heuristic of the evaluated input */ - private Truthness evaluateNested(Object value, - Function elementHeuristic) { + private Truthness evaluateWithArrayUnwrapping(Object value, + Function elementHeuristic) { Objects.requireNonNull(elementHeuristic); if (!(value instanceof List)) { + // value is not an array. Inspect only this value. return elementHeuristic.apply(value); + } else { + // value is an array. Inspect all elements. + List values = (List) value; + if (values.isEmpty()) { + return C_FALSE; + } + Truthness orAggregation = buildOrAggregationTruthness(values.stream() + .map(elementHeuristic) + .toArray(Truthness[]::new)); + return buildSafeScaledTruthness(orAggregation); } - - List values = (List) value; - if (values.isEmpty()) { - return C_FALSE; - } - - Truthness orAggregation = buildOrAggregationTruthness(values.stream() - .map(elementHeuristic) - .toArray(Truthness[]::new)); - return buildSafeScaledTruthness(orAggregation); } private Truthness computeHeuristic(OrOperation operation, Object document) { @@ -504,79 +409,18 @@ private Truthness evaluate(InOperation operation, Object actualValue) { Objects.requireNonNull(operation); List expectedValueList = operation.getValues(); - final Truthness res = computeHeuristicInOperation(actualValue, expectedValueList); + final Truthness res = helper.computeHeuristicInOperation(actualValue, expectedValueList); return res; } - /** - * Computes the heuristic score for determining if an element is contained in a list. - * The method evaluates the presence of the given element in the provided list and calculates - * a heuristic truthness score based on the comparison results. - * - * @param element the element whose presence in the list is to be evaluated; can be null. - * @param list the list of objects to search; must not be null. - * @return a Truthness object representing the heuristic score of the element's presence in the list. - */ - private Truthness computeHeuristicContainsElement(Object element, List list) { - Objects.requireNonNull(list); - - if (list.isEmpty()) { - return C_FALSE; - } else { - Truthness res = buildOrAggregationTruthness(list.stream() - .map(expectedValue -> compareNullableValues(expectedValue, - SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO, element - )) - .toArray(Truthness[]::new)); - return buildSafeScaledTruthness(res); - } - } - private Truthness evaluate(NotInOperation operation, Object actualValue) { Objects.requireNonNull(operation); List expectedValueList = operation.getValues(); - final Truthness res = computeHeuristicInOperation(actualValue, expectedValueList); + final Truthness res = helper.computeHeuristicInOperation(actualValue, expectedValueList); return res.invert(); } - private Truthness computeHeuristicInOperation(Object actualValue, List expectedValueList) { - final Truthness res; - if (actualValue instanceof List) { - List actualValueList = (List) actualValue; - // first we try to match the actualValueList as a whole with any element of the expectedValueList - Truthness[] arrayOfTruthnesses = expectedValueList.stream() - .filter(expectedValueListElement -> expectedValueListElement instanceof List) - .map(expectedValueListElement -> (List) expectedValueListElement) - .map(expectedValueInnerListElement -> - computeHeuristicListEquality(expectedValueInnerListElement, actualValueList)) - .toArray(Truthness[]::new); - final Truthness isTheValueListEqualToAnyExpectedValueList; - if (arrayOfTruthnesses.length > 0) { - isTheValueListEqualToAnyExpectedValueList = buildOrAggregationTruthness(arrayOfTruthnesses); - } else { - isTheValueListEqualToAnyExpectedValueList = C_FALSE; - } - - if (isTheValueListEqualToAnyExpectedValueList.isFalse()) { - // if we fail, we try to match each element of the actualValueList with any element of the expectedValueList - if (actualValueList.isEmpty()) { - res = C_FALSE; - } else { - Truthness orAggregation = buildOrAggregationTruthness(actualValueList.stream() - .map(actualValueListElement -> computeHeuristicContainsElement(actualValueListElement, expectedValueList)) - .toArray(Truthness[]::new)); - res = buildSafeScaledTruthness(orAggregation); - } - } else { - res = isTheValueListEqualToAnyExpectedValueList; - } - } else { - res = computeHeuristicContainsElement(actualValue, expectedValueList); - } - return res; - } - private Truthness evaluate(AllOperation operation, Object actualValue) { Objects.requireNonNull(operation); @@ -596,7 +440,7 @@ private Truthness evaluate(AllOperation operation, Object actualValue) { if (expectedValues.size() != 1) { return C_FALSE; } else { - return compareNullableValues( + return helper.compareNullableValues( expectedValues.get(0), SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO, actualValue ); @@ -609,7 +453,7 @@ private Truthness evaluate(AllOperation operation, Object actualValue) { } Truthness res = buildAndAggregationTruthness(expectedValues.stream() - .map(expectedValue -> computeHeuristicContainsElement(expectedValue, actualValues)) + .map(expectedValue -> helper.computeHeuristicContainsElement(expectedValue, actualValues)) .toArray(Truthness[]::new)); return buildSafeScaledTruthness(res); } @@ -639,7 +483,7 @@ private Truthness evaluate(ElemMatchOperation operation, Object actualValue) { return C_FALSE; } else { Truthness orAggregation = buildOrAggregationTruthness(actualList.stream() - .map(listElement -> computeHeuristicOnElement( + .map(listElement -> evaluateOnArrayElement( operation.getCondition(), listElement)) .toArray(Truthness[]::new)); return buildSafeScaledTruthness(orAggregation); @@ -647,14 +491,14 @@ private Truthness evaluate(ElemMatchOperation operation, Object actualValue) { } } - private Truthness computeHeuristicOnElement(QueryOperation condition, Object element) { + private Truthness evaluateOnArrayElement(QueryOperation condition, Object element) { if (condition instanceof AndOperation) { return buildAndAggregationTruthness(((AndOperation) condition).getConditions().stream() - .map(child -> computeHeuristicOnElement(child, element)) + .map(child -> evaluateOnArrayElement(child, element)) .toArray(Truthness[]::new)); } else if (condition instanceof OrOperation) { return buildOrAggregationTruthness(((OrOperation) condition).getConditions().stream() - .map(child -> computeHeuristicOnElement(child, element)) + .map(child -> evaluateOnArrayElement(child, element)) .toArray(Truthness[]::new)); } else if (condition instanceof QueryOperationWithField && "$".equals(((QueryOperationWithField) condition).getFieldName())) { @@ -685,7 +529,7 @@ private Truthness computeHeuristic(ExistsOperation operation, Object input) { } else { Truthness orTruthness = buildOrAggregationTruthness(actualFieldNames.stream() .map(actualFieldName -> - compareNonNullValues(actualFieldName, + helper.compareNonNullValues(actualFieldName, SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO, expectedFieldName )) .toArray(Truthness[]::new)); @@ -714,65 +558,40 @@ private Truthness evaluate(ModOperation operation, Object actualValue) { long divisor = operation.getDivisor().longValue(); long expectedRemainder = operation.getRemainder().longValue(); - return evaluateNested(actualValue, - value -> computeHeuristicModOnSingleValue(value, divisor, expectedRemainder)); + return evaluateWithArrayUnwrapping(actualValue, + value -> helper.computeHeuristicModOnSingleValue(value, divisor, expectedRemainder)); } - private Truthness computeHeuristicModOnSingleValue(Object value, long divisor, long expectedRemainder) { - if (!(value instanceof Number)) { - return C_FALSE; - } - - long actualRemainder = ((Number) value).longValue() % divisor; - Truthness res = getEqualityTruthness(actualRemainder, expectedRemainder); - return buildSafeScaledTruthness(res); + private Truthness evaluate(BitsAllClearOperation operation, Object actualValue) { + Objects.requireNonNull(operation); + return evaluateWithArrayUnwrapping(actualValue, + value -> helper.evaluateBitsAllClearOperation(operation.getBitmask(), value)); } - private Truthness evaluate(BitsOperation operation, Object actualValue) { + private Truthness evaluate(BitsAnyClearOperation operation, Object actualValue) { Objects.requireNonNull(operation); - return evaluateNested(actualValue, - value -> evaluateOnSingleValue(operation, value)); + return evaluateWithArrayUnwrapping(actualValue, + value -> helper.evaluateBitsAnyClearOperation(operation.getBitmask(), value)); } - private Truthness evaluateOnSingleValue(BitsOperation operation, - Object value) { + private Truthness evaluate(BitsAllSetOperation operation, Object actualValue) { Objects.requireNonNull(operation); + return evaluateWithArrayUnwrapping(actualValue, + value -> helper.evaluateBitsAllSetOperation(operation.getBitmask(), value)); + } - if (!(value instanceof Number)) { - return C_FALSE; - } - - final OptionalLong integralValue = getIntegralLongValue((Number) value); - if (!integralValue.isPresent()) { - return C_FALSE; - } + private Truthness evaluate(BitsAnySetOperation operation, Object actualValue) { + Objects.requireNonNull(operation); + return evaluateWithArrayUnwrapping(actualValue, + value -> helper.evaluateBitsAnySetOperation(operation.getBitmask(), value)); - final long bitmask = operation.getBitmask(); - final long maskedValue = integralValue.getAsLong() & bitmask; - final int numberOfSetBitsInMaskedValue = Long.bitCount(maskedValue); - final int numberOfBitsInMask = Long.bitCount(bitmask); - if (operation instanceof BitsAllClearOperation) { - Truthness equalityTruthness = getEqualityTruthness(numberOfSetBitsInMaskedValue, 0); - return buildSafeScaledTruthness(equalityTruthness); - } else if (operation instanceof BitsAllSetOperation) { - Truthness equalityTruthness = getEqualityTruthness(numberOfSetBitsInMaskedValue, numberOfBitsInMask); - return buildSafeScaledTruthness(equalityTruthness); - } else if (operation instanceof BitsAnyClearOperation) { - Truthness lessThanTruthness = getLessThanTruthness(numberOfSetBitsInMaskedValue, numberOfBitsInMask); - return buildSafeScaledTruthness(lessThanTruthness); - } else if (operation instanceof BitsAnySetOperation) { - Truthness lessThanTruthness = getLessThanTruthness(0, numberOfSetBitsInMaskedValue); - return buildSafeScaledTruthness(lessThanTruthness); - } else { - throw new IllegalArgumentException("Unsupported BitsOperation type: " + operation.getClass().getName()); - } } - private Truthness computeHeuristicOnValue(NotOperation operation, Object actualValue) { + private Truthness evaluate(NotOperation operation, Object actualValue) { Objects.requireNonNull(operation); QueryOperation condition = operation.getCondition(); - Truthness conditionTruthness = computeHeuristicOnElement(condition, actualValue); + Truthness conditionTruthness = evaluateOnArrayElement(condition, actualValue); return conditionTruthness.invert(); } @@ -814,93 +633,14 @@ private Truthness evaluate(NearSphereOperation operation, Object actualValue) { final double longitude = operation.getLongitude(); final double latitude = operation.getLatitude(); - return evaluate(operation, longitude, latitude, actualValue, SPHERICAL); - } - - - private static Truthness evaluate(AbstractProximityOperation abstractProximityOperation, - double longitude, - double latitude, - Object actualValue, - GeoSpatialModel geoSpatialModel) { + final double maxDistance = operation.hasMaxDistance() ? operation.getMaxDistance() : Double.MAX_VALUE; + final double minDistance = operation.hasMinDistance() ? operation.getMinDistance() : 0.0; - Objects.requireNonNull(abstractProximityOperation); - double x1 = geoSpatialModel == SPHERICAL ? Math.toRadians(longitude) : longitude; - double y1 = geoSpatialModel == SPHERICAL ? Math.toRadians(latitude) : latitude; - double x2; - double y2; - - /* - GeoJSON Point in document. - type key is case-sensitive. - (https://datatracker.ietf.org/doc/html/rfc7946#section-1.4) - */ - if (isBsonDocument(actualValue) - && GeoJsonUtils.isGeoJsonPoint(actualValue)) { - GeoJsonPoint geoJsonPoint = GeoJsonUtils.toGeoJsonPoint(actualValue); - x2 = geoSpatialModel == SPHERICAL - ? Math.toRadians(geoJsonPoint.getLongitude()) - : geoJsonPoint.getLongitude(); - y2 = geoSpatialModel == SPHERICAL - ? Math.toRadians(geoJsonPoint.getLatitude()) - : geoJsonPoint.getLatitude(); - } else { - return C_FALSE; - } - double distanceBetweenPoints = getDistanceBetweenPoints(x1, y1, x2, y2, geoSpatialModel); - double max = abstractProximityOperation.hasMaxDistance() - ? abstractProximityOperation.getMaxDistance() - : Double.MAX_VALUE; - - double min = abstractProximityOperation.hasMinDistance() - ? abstractProximityOperation.getMinDistance() - : 0.0; - - if (min <= distanceBetweenPoints - && distanceBetweenPoints <= max) { - return TRUE_C; - } - - return (distanceBetweenPoints > max) - ? getEqualityTruthness(distanceBetweenPoints, max) - : getEqualityTruthness(distanceBetweenPoints, min); - } - - private Truthness compareNonNullLists(List leftList, SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType, List rightList) { - Objects.requireNonNull(leftList); - Objects.requireNonNull(rightList); - - final Truthness truthness = computeHeuristicListEquality(leftList, rightList); - switch (comparisonOperatorType) { - case EQUALS_TO: - return truthness; - case NOT_EQUALS_TO: - return truthness.invert(); - default: - throw new IllegalArgumentException("Unsupported binary operator: " + comparisonOperatorType); - } - } - - private Truthness computeHeuristicListEquality(List actualList, List expectedList) { - - if (actualList.size() != expectedList.size()) { - return C_FALSE; - } - - if (actualList.isEmpty() && expectedList.isEmpty()) { - return TRUE_C; - } - - Truthness[] arrayOfTruthnesses = new Truthness[actualList.size()]; - for (int i = 0; i < actualList.size(); i++) { - arrayOfTruthnesses[i] = compareNullableValues( - actualList.get(i), - SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO, expectedList.get(i) - ); - } - Truthness unscaledTruthness = buildAndAggregationTruthness(arrayOfTruthnesses); - final Truthness truthness = buildSafeScaledTruthness(unscaledTruthness); - return truthness; + return helper.evaluate( + minDistance, maxDistance, + longitude, latitude, + actualValue, + SPHERICAL); } From 617861e8422cfe27d5d9c91b4a3e1f77dec92649 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Fri, 11 Sep 2026 15:57:21 -0300 Subject: [PATCH 19/53] Refactor Mongo heuristic calculations: extract helper class, simplify logic, and improve modularity for comparison and bitwise operations --- .../MongoHeuristicsCalculatorHelper.java | 370 +++++++++++++++++- 1 file changed, 364 insertions(+), 6 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java index eaf4c1322b..2697bd3f13 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java @@ -1,16 +1,29 @@ package org.evomaster.client.java.controller.mongo; +import org.evomaster.client.java.controller.mongo.geometry.GeoJsonPoint; +import org.evomaster.client.java.controller.mongo.geometry.GeoJsonUtils; +import org.evomaster.client.java.controller.mongo.utils.BsonHelper; +import org.evomaster.client.java.controller.mongo.utils.MongoUtils; import org.evomaster.client.java.distance.heuristics.Truthness; import org.evomaster.client.java.instrumentation.coverage.methodreplacement.RegexDistanceUtils; +import org.evomaster.client.java.sql.heuristic.SqlExpressionEvaluator; import org.evomaster.client.java.sql.internal.TaintHandler; +import java.util.Date; +import java.util.List; import java.util.Objects; +import java.util.OptionalLong; import java.util.regex.Matcher; import java.util.regex.Pattern; -import static org.evomaster.client.java.distance.heuristics.TruthnessUtils.buildScaledTruthness; +import static org.evomaster.client.java.controller.mongo.utils.BsonHelper.isBsonDocument; +import static org.evomaster.client.java.controller.mongo.utils.MongoUtils.GeoSpatialModel.SPHERICAL; +import static org.evomaster.client.java.controller.mongo.utils.MongoUtils.getDistanceBetweenPoints; +import static org.evomaster.client.java.controller.mongo.utils.MongoUtils.getIntegralLongValue; +import static org.evomaster.client.java.distance.heuristics.TruthnessUtils.*; +import static org.evomaster.client.java.sql.heuristic.ConversionHelper.convertToInstant; -public abstract class MongoHeuristicsCalculatorHelper { +public class MongoHeuristicsCalculatorHelper { // TODO these constants should be replaced by DistanceHelper constants public static final double C = 0.1; @@ -18,6 +31,12 @@ public abstract class MongoHeuristicsCalculatorHelper { // TODO These constants should be refactored by TruthnessUtils constants public static final Truthness TRUE_C = new Truthness(1.0, C); + private final TaintHandler taintHandler; + + public MongoHeuristicsCalculatorHelper(TaintHandler taintHandler) { + this.taintHandler = taintHandler; + } + static Truthness buildSafeScaledTruthness(double maxOfTrue) { if (maxOfTrue == 1.0) { return TRUE_C; @@ -26,6 +45,7 @@ static Truthness buildSafeScaledTruthness(double maxOfTrue) { } } + /** * Evaluates whether the given input string matches the provided regular expression pattern. * The result is represented as a {@link Truthness} object, which captures both @@ -33,13 +53,13 @@ static Truthness buildSafeScaledTruthness(double maxOfTrue) { * Optionally, a {@link TaintHandler} can be used to handle tainting information * related to the regular expression processing. * - * @param inputValue the input string to be matched against the regular expression - * @param pattern the compiled {@link Pattern} representing the regular expression to match + * @param inputValue the input string to be matched against the regular expression + * @param pattern the compiled {@link Pattern} representing the regular expression to match * @param taintHandler an optional implementation of {@link TaintHandler} to handle taint-related processing; * may be null if taint handling is not required * @return a {@link Truthness} object representing the result of the evaluation, - * where one component (true or false) is 1 to reflect the match status, and the other - * captures the approximation in cases of non-exact matches + * where one component (true or false) is 1 to reflect the match status, and the other + * captures the approximation in cases of non-exact matches */ static Truthness evaluateRegularExpression(String inputValue, Pattern pattern, TaintHandler taintHandler) { final String patternString = pattern.pattern(); @@ -76,4 +96,342 @@ static Truthness buildSafeScaledTruthness(Truthness truthness) { return buildSafeScaledTruthness(truthness.getOfTrue()); } + + Truthness compareNonNullLists(List leftList, SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType, List rightList) { + Objects.requireNonNull(leftList); + Objects.requireNonNull(rightList); + + final Truthness truthness = computeHeuristicListEquality(leftList, rightList); + switch (comparisonOperatorType) { + case EQUALS_TO: + return truthness; + case NOT_EQUALS_TO: + return truthness.invert(); + default: + throw new IllegalArgumentException("Unsupported binary operator: " + comparisonOperatorType); + } + } + + Truthness computeHeuristicListEquality(List actualList, List expectedList) { + + if (actualList.size() != expectedList.size()) { + return C_FALSE; + } + + if (actualList.isEmpty() && expectedList.isEmpty()) { + return TRUE_C; + } + + Truthness[] arrayOfTruthnesses = new Truthness[actualList.size()]; + for (int i = 0; i < actualList.size(); i++) { + arrayOfTruthnesses[i] = compareNullableValues( + actualList.get(i), + SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO, expectedList.get(i) + ); + } + Truthness unscaledTruthness = buildAndAggregationTruthness(arrayOfTruthnesses); + final Truthness truthness = buildSafeScaledTruthness(unscaledTruthness); + return truthness; + } + + Truthness compareNonNullValues(Object leftValue, + SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType, + Object rightValue) { + Objects.requireNonNull(leftValue); + Objects.requireNonNull(rightValue); + + final Truthness truthnessOfComparison; + if (leftValue instanceof Number && rightValue instanceof Number) { + final Number actualNumber = (Number) leftValue; + final Number expectedNumber = (Number) rightValue; + truthnessOfComparison = compareNumberValues(actualNumber, comparisonOperatorType, expectedNumber); + + } else if (leftValue instanceof String && rightValue instanceof String) { + String actualString = (String) leftValue; + String expectedString = (String) rightValue; + if (taintHandler != null && comparisonOperatorType == SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO) { + taintHandler.handleTaintForStringEquals(actualString, expectedString, false); + } + truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForStringComparison(actualString, expectedString, comparisonOperatorType); + + } else if (leftValue instanceof Boolean && rightValue instanceof Boolean) { + int actualIntValue = toIntValue((Boolean) leftValue); + int expectedIntValue = toIntValue((Boolean) rightValue); + truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForNumberComparison( + actualIntValue, expectedIntValue, comparisonOperatorType); + + } else if (leftValue instanceof List && rightValue instanceof List) { + truthnessOfComparison = compareNonNullLists((List) leftValue, comparisonOperatorType, (List) rightValue); + + } else if (leftValue instanceof Date && rightValue instanceof Date) { + truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForInstantComparison(convertToInstant(leftValue), convertToInstant(rightValue), comparisonOperatorType); + + + } else if (BsonHelper.isBsonTimestamp(leftValue) && BsonHelper.isBsonTimestamp(rightValue)) { + long actualTimestamp = BsonHelper.getBsonTimestampValue(leftValue); + long expectedTimestamp = BsonHelper.getBsonTimestampValue(rightValue); + truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForNumberComparison(actualTimestamp, expectedTimestamp, comparisonOperatorType); + + } else if (BsonHelper.isObjectId(leftValue) || BsonHelper.isObjectId(rightValue)) { + String actualString = leftValue.toString(); + String expectedString = rightValue.toString(); + if (taintHandler != null && comparisonOperatorType == SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO) { + taintHandler.handleTaintForStringEquals(actualString, expectedString, false); + } + truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForStringComparison(actualString, expectedString, comparisonOperatorType); + + } else { + // If both types are supported, but no actual comparison logic is defined, + // we considered them to be incompatible, therefore the comparison returns true + // only if the comparison operator is NOT_EQUALS_TO. Otherwise returns false. + truthnessOfComparison = comparisonOperatorType == SqlExpressionEvaluator.ComparisonOperatorType.NOT_EQUALS_TO ? TRUE_C : C_FALSE; + } + return truthnessOfComparison; + } + + static Truthness compareNumberValues(Number leftNumber, SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType, Number rightNumber) { + Objects.requireNonNull(leftNumber); + Objects.requireNonNull(rightNumber); + Objects.requireNonNull(comparisonOperatorType); + + double leftValueAsDouble = leftNumber.doubleValue(); + double rightValueAsDouble = rightNumber.doubleValue(); + + if (Double.isNaN(leftValueAsDouble) || Double.isNaN(rightValueAsDouble)) { + // handle case when NaN is involved in the comparison + switch (comparisonOperatorType) { + case EQUALS_TO: { + return (Double.isNaN(leftValueAsDouble) && Double.isNaN(rightValueAsDouble)) ? + TRUE_C : C_FALSE; + } + case NOT_EQUALS_TO: { + return (!Double.isNaN(leftValueAsDouble) || !Double.isNaN(rightValueAsDouble)) ? + TRUE_C : C_FALSE; + } + case GREATER_THAN: + case GREATER_THAN_EQUALS: + case MINOR_THAN: + case MINOR_THAN_EQUALS: { + return C_FALSE; + } + default: + throw new IllegalArgumentException("Unsupported comparison operator type: " + comparisonOperatorType); + } + } else { + // if both values are not NaN, we can use the standard comparison logic + final Truthness truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForNumberComparison(leftNumber, rightNumber, comparisonOperatorType); + return truthnessOfComparison; + } + } + + Truthness compareNullableValues(Object leftValue, SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType, Object rightValue) { + if (leftValue == null || rightValue == null) { + switch (comparisonOperatorType) { + case EQUALS_TO: + return (leftValue == null && rightValue == null) ? TRUE_C : C_FALSE; + case NOT_EQUALS_TO: + return (leftValue == null && rightValue == null) ? C_FALSE : TRUE_C; + case GREATER_THAN: + case GREATER_THAN_EQUALS: + case MINOR_THAN: + case MINOR_THAN_EQUALS: + return C_FALSE; + default: + throw new IllegalArgumentException("Unsupported comparison operator type: " + comparisonOperatorType); + } + } else { + Truthness valTruthness = compareNonNullValues(rightValue, + comparisonOperatorType, leftValue + ); + return buildSafeScaledTruthness(valTruthness); + } + } + + /** + * Computes the heuristic score for determining if an element is contained in a list. + * The method evaluates the presence of the given element in the provided list and calculates + * a heuristic truthness score based on the comparison results. + * + * @param element the element whose presence in the list is to be evaluated; can be null. + * @param list the list of objects to search; must not be null. + * @return a Truthness object representing the heuristic score of the element's presence in the list. + */ + Truthness computeHeuristicContainsElement(Object element, List list) { + Objects.requireNonNull(list); + + if (list.isEmpty()) { + return C_FALSE; + } else { + Truthness res = buildOrAggregationTruthness(list.stream() + .map(expectedValue -> compareNullableValues(expectedValue, + SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO, element + )) + .toArray(Truthness[]::new)); + return buildSafeScaledTruthness(res); + } + } + + Truthness computeHeuristicInOperation(Object actualValue, List expectedValueList) { + final Truthness res; + if (actualValue instanceof List) { + List actualValueList = (List) actualValue; + // first we try to match the actualValueList as a whole with any element of the expectedValueList + Truthness[] arrayOfTruthnesses = expectedValueList.stream() + .filter(expectedValueListElement -> expectedValueListElement instanceof List) + .map(expectedValueListElement -> (List) expectedValueListElement) + .map(expectedValueInnerListElement -> + computeHeuristicListEquality(expectedValueInnerListElement, actualValueList)) + .toArray(Truthness[]::new); + final Truthness isTheValueListEqualToAnyExpectedValueList; + if (arrayOfTruthnesses.length > 0) { + isTheValueListEqualToAnyExpectedValueList = buildOrAggregationTruthness(arrayOfTruthnesses); + } else { + isTheValueListEqualToAnyExpectedValueList = C_FALSE; + } + + if (isTheValueListEqualToAnyExpectedValueList.isFalse()) { + // if we fail, we try to match each element of the actualValueList with any element of the expectedValueList + if (actualValueList.isEmpty()) { + res = C_FALSE; + } else { + Truthness orAggregation = buildOrAggregationTruthness(actualValueList.stream() + .map(actualValueListElement -> computeHeuristicContainsElement(actualValueListElement, expectedValueList)) + .toArray(Truthness[]::new)); + res = buildSafeScaledTruthness(orAggregation); + } + } else { + res = isTheValueListEqualToAnyExpectedValueList; + } + } else { + res = computeHeuristicContainsElement(actualValue, expectedValueList); + } + return res; + } + + Truthness computeHeuristicModOnSingleValue(Object value, long divisor, long expectedRemainder) { + if (!(value instanceof Number)) { + return C_FALSE; + } + + long actualRemainder = ((Number) value).longValue() % divisor; + Truthness res = getEqualityTruthness(actualRemainder, expectedRemainder); + return buildSafeScaledTruthness(res); + } + + + Truthness evaluateBitsAllClearOperation(long bitmask, + Object value) { + if (!(value instanceof Number)) { + return C_FALSE; + } + final OptionalLong integralValue = getIntegralLongValue((Number) value); + if (!integralValue.isPresent()) { + return C_FALSE; + } + + final long maskedValue = integralValue.getAsLong() & bitmask; + final int numberOfSetBitsInMaskedValue = Long.bitCount(maskedValue); + final int numberOfBitsInMask = Long.bitCount(bitmask); + Truthness equalityTruthness = getEqualityTruthness(numberOfSetBitsInMaskedValue, 0); + return buildSafeScaledTruthness(equalityTruthness); + } + + Truthness evaluateBitsAllSetOperation(long bitmask, + Object value) { + + if (!(value instanceof Number)) { + return C_FALSE; + } + final OptionalLong integralValue = getIntegralLongValue((Number) value); + if (!integralValue.isPresent()) { + return C_FALSE; + } + + final long maskedValue = integralValue.getAsLong() & bitmask; + final int numberOfSetBitsInMaskedValue = Long.bitCount(maskedValue); + final int numberOfBitsInMask = Long.bitCount(bitmask); + + Truthness equalityTruthness = getEqualityTruthness(numberOfSetBitsInMaskedValue, numberOfBitsInMask); + return buildSafeScaledTruthness(equalityTruthness); + } + + Truthness evaluateBitsAnyClearOperation(long bitmask, + Object value) { + + if (!(value instanceof Number)) { + return C_FALSE; + } + + final OptionalLong integralValue = getIntegralLongValue((Number) value); + if (!integralValue.isPresent()) { + return C_FALSE; + } + + final long maskedValue = integralValue.getAsLong() & bitmask; + final int numberOfSetBitsInMaskedValue = Long.bitCount(maskedValue); + final int numberOfBitsInMask = Long.bitCount(bitmask); + + Truthness lessThanTruthness = getLessThanTruthness(numberOfSetBitsInMaskedValue, numberOfBitsInMask); + return buildSafeScaledTruthness(lessThanTruthness); + } + + Truthness evaluateBitsAnySetOperation(long bitmask, + Object value) { + + if (!(value instanceof Number)) { + return C_FALSE; + } + + final OptionalLong integralValue = getIntegralLongValue((Number) value); + if (!integralValue.isPresent()) { + return C_FALSE; + } + + final long maskedValue = integralValue.getAsLong() & bitmask; + final int numberOfSetBitsInMaskedValue = Long.bitCount(maskedValue); + Truthness lessThanTruthness = getLessThanTruthness(0, numberOfSetBitsInMaskedValue); + return buildSafeScaledTruthness(lessThanTruthness); + } + + static Truthness evaluate(double minDistance, + double maxDistance, + double longitude, + double latitude, + Object actualValue, + MongoUtils.GeoSpatialModel geoSpatialModel) { + + double x1 = geoSpatialModel == SPHERICAL ? Math.toRadians(longitude) : longitude; + double y1 = geoSpatialModel == SPHERICAL ? Math.toRadians(latitude) : latitude; + double x2; + double y2; + + /* + GeoJSON Point in document. + type key is case-sensitive. + (https://datatracker.ietf.org/doc/html/rfc7946#section-1.4) + */ + if (isBsonDocument(actualValue) + && GeoJsonUtils.isGeoJsonPoint(actualValue)) { + GeoJsonPoint geoJsonPoint = GeoJsonUtils.toGeoJsonPoint(actualValue); + x2 = geoSpatialModel == SPHERICAL + ? Math.toRadians(geoJsonPoint.getLongitude()) + : geoJsonPoint.getLongitude(); + y2 = geoSpatialModel == SPHERICAL + ? Math.toRadians(geoJsonPoint.getLatitude()) + : geoJsonPoint.getLatitude(); + } else { + return C_FALSE; + } + double distanceBetweenPoints = getDistanceBetweenPoints(x1, y1, x2, y2, geoSpatialModel); + + if (minDistance <= distanceBetweenPoints + && distanceBetweenPoints <= maxDistance) { + return TRUE_C; + } + + return (distanceBetweenPoints > maxDistance) + ? getEqualityTruthness(distanceBetweenPoints, maxDistance) + : getEqualityTruthness(distanceBetweenPoints, minDistance); + } + } From be1815da425faf6dea3dc3179d0a3e182a8bec23 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Sat, 12 Sep 2026 08:10:20 -0300 Subject: [PATCH 20/53] Rename methods in `MongoHeuristicsCalculatorHelper` for improved clarity and consistency. --- .../mongo/MongoHeuristicsCalculator.java | 6 +-- .../MongoHeuristicsCalculatorHelper.java | 39 ++++++++++--------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java index 52edfcbc1e..521dcf2008 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java @@ -234,7 +234,7 @@ private Truthness evaluate(NearOperation operation, Object actualValue) { final double minDistance = operation.hasMinDistance() ? operation.getMinDistance() : 0.0; final double maxDistance = operation.hasMaxDistance() ? operation.getMaxDistance() : Double.MAX_VALUE; - return helper.evaluate( + return helper.evaluateDistanceBetweenPoints( minDistance, maxDistance, longitude, latitude, actualValue, @@ -559,7 +559,7 @@ private Truthness evaluate(ModOperation operation, Object actualValue) { long divisor = operation.getDivisor().longValue(); long expectedRemainder = operation.getRemainder().longValue(); return evaluateWithArrayUnwrapping(actualValue, - value -> helper.computeHeuristicModOnSingleValue(value, divisor, expectedRemainder)); + value -> helper.evaluateMod(divisor, expectedRemainder, value)); } private Truthness evaluate(BitsAllClearOperation operation, Object actualValue) { @@ -636,7 +636,7 @@ private Truthness evaluate(NearSphereOperation operation, Object actualValue) { final double maxDistance = operation.hasMaxDistance() ? operation.getMaxDistance() : Double.MAX_VALUE; final double minDistance = operation.hasMinDistance() ? operation.getMinDistance() : 0.0; - return helper.evaluate( + return helper.evaluateDistanceBetweenPoints( minDistance, maxDistance, longitude, latitude, actualValue, diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java index 2697bd3f13..34019eb3c8 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java @@ -101,7 +101,7 @@ Truthness compareNonNullLists(List leftList, SqlExpressionEvaluator.Compariso Objects.requireNonNull(leftList); Objects.requireNonNull(rightList); - final Truthness truthness = computeHeuristicListEquality(leftList, rightList); + final Truthness truthness = evaluateListEquality(leftList, rightList); switch (comparisonOperatorType) { case EQUALS_TO: return truthness; @@ -112,7 +112,7 @@ Truthness compareNonNullLists(List leftList, SqlExpressionEvaluator.Compariso } } - Truthness computeHeuristicListEquality(List actualList, List expectedList) { + Truthness evaluateListEquality(List actualList, List expectedList) { if (actualList.size() != expectedList.size()) { return C_FALSE; @@ -280,7 +280,7 @@ Truthness computeHeuristicInOperation(Object actualValue, List expectedValueL .filter(expectedValueListElement -> expectedValueListElement instanceof List) .map(expectedValueListElement -> (List) expectedValueListElement) .map(expectedValueInnerListElement -> - computeHeuristicListEquality(expectedValueInnerListElement, actualValueList)) + evaluateListEquality(expectedValueInnerListElement, actualValueList)) .toArray(Truthness[]::new); final Truthness isTheValueListEqualToAnyExpectedValueList; if (arrayOfTruthnesses.length > 0) { @@ -308,12 +308,12 @@ Truthness computeHeuristicInOperation(Object actualValue, List expectedValueL return res; } - Truthness computeHeuristicModOnSingleValue(Object value, long divisor, long expectedRemainder) { - if (!(value instanceof Number)) { + Truthness evaluateMod(long divisor, long expectedRemainder, Object actualValue) { + if (!(actualValue instanceof Number)) { return C_FALSE; } - long actualRemainder = ((Number) value).longValue() % divisor; + long actualRemainder = ((Number) actualValue).longValue() % divisor; Truthness res = getEqualityTruthness(actualRemainder, expectedRemainder); return buildSafeScaledTruthness(res); } @@ -324,14 +324,15 @@ Truthness evaluateBitsAllClearOperation(long bitmask, if (!(value instanceof Number)) { return C_FALSE; } - final OptionalLong integralValue = getIntegralLongValue((Number) value); + final Number number = (Number) value; + final OptionalLong integralValue = getIntegralLongValue(number); + if (!integralValue.isPresent()) { return C_FALSE; } final long maskedValue = integralValue.getAsLong() & bitmask; final int numberOfSetBitsInMaskedValue = Long.bitCount(maskedValue); - final int numberOfBitsInMask = Long.bitCount(bitmask); Truthness equalityTruthness = getEqualityTruthness(numberOfSetBitsInMaskedValue, 0); return buildSafeScaledTruthness(equalityTruthness); } @@ -342,7 +343,8 @@ Truthness evaluateBitsAllSetOperation(long bitmask, if (!(value instanceof Number)) { return C_FALSE; } - final OptionalLong integralValue = getIntegralLongValue((Number) value); + final Number number = (Number) value; + final OptionalLong integralValue = getIntegralLongValue(number); if (!integralValue.isPresent()) { return C_FALSE; } @@ -361,8 +363,8 @@ Truthness evaluateBitsAnyClearOperation(long bitmask, if (!(value instanceof Number)) { return C_FALSE; } - - final OptionalLong integralValue = getIntegralLongValue((Number) value); + final Number number = (Number) value; + final OptionalLong integralValue = getIntegralLongValue(number); if (!integralValue.isPresent()) { return C_FALSE; } @@ -382,7 +384,8 @@ Truthness evaluateBitsAnySetOperation(long bitmask, return C_FALSE; } - final OptionalLong integralValue = getIntegralLongValue((Number) value); + Number number = (Number) value; + final OptionalLong integralValue = getIntegralLongValue(number); if (!integralValue.isPresent()) { return C_FALSE; } @@ -393,12 +396,12 @@ Truthness evaluateBitsAnySetOperation(long bitmask, return buildSafeScaledTruthness(lessThanTruthness); } - static Truthness evaluate(double minDistance, - double maxDistance, - double longitude, - double latitude, - Object actualValue, - MongoUtils.GeoSpatialModel geoSpatialModel) { + static Truthness evaluateDistanceBetweenPoints(double minDistance, + double maxDistance, + double longitude, + double latitude, + Object actualValue, + MongoUtils.GeoSpatialModel geoSpatialModel) { double x1 = geoSpatialModel == SPHERICAL ? Math.toRadians(longitude) : longitude; double y1 = geoSpatialModel == SPHERICAL ? Math.toRadians(latitude) : latitude; From 02c4534cd64a81c9c627ed249db8ba6896ee6d20 Mon Sep 17 00:00:00 2001 From: LautaroPetaccio Date: Mon, 14 Sep 2026 10:31:34 -0300 Subject: [PATCH 21/53] Add tests for Mongo semantics the heuristics calculator does not reproduce Twenty queries where the calculator disagrees with the database, and three neighbouring cases that it already answers correctly, kept as guards so that a fix can be told apart from a regression in the path next to it. They extend the section at the end of MongoHeuristicsCalculatorTest, which was written for exactly this, and follow its convention: every expected value was taken from a MongoDB 7.0.41 server answering the same query against the same document, one test per defect, and @Disabled with the reason on the ones that fail today. Where the database refuses the query outright there is no answer to copy, so those tests assert that the calculator answers false without throwing, that being the only answer available to it. Four of them concern values compareNonNullValues has no branch for, being two sub-documents or two binary values, which reach the case for types that cannot be compared. That answers false for $eq, missing the plainest nested-object query there is, and true for $ne and $nin, which is a match the database does not have. On master these threw instead, so this is a step forward that stops short. Six of the queries make the calculator throw, and MongoHandler catches nothing around computeDistanceDocuments, so the exception costs the action its whole ExtraHeuristicsDto, the SQL heuristics included. Two of those six are queries the database answers normally: $regex against an array holding no strings, and $not holding a bare regex. --- .../mongo/MongoHeuristicsCalculatorTest.java | 451 ++++++++++++++++++ 1 file changed, 451 insertions(+) diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 3efc888435..80373e8f8b 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -2114,4 +2114,455 @@ public void testNotWithMoreThanOneInnerOperator() { assertTrue(calculator.computeHeuristicDocument(nested, doc).isFalse()); assertTrue(calculator.computeHeuristicDocument(nested, new Document("a", 5)).isTrue()); } + + /* + ================================================================================ + Further cases, in the same form: every expected value comes from a MongoDB + 7.0.41 server answering that query against that document, one test per defect, + and @Disabled on the ones that fail today. + + Where the database refuses the query outright there is no answer to copy, so + those tests assert that the calculator answers false without throwing, that + being the only answer available to it. + ================================================================================ + */ + + @Test + @Disabled("$regex against an array holding no strings throws IllegalArgumentException") + public void testRegexAgainstAnArrayWithoutStringElements() { + /* + mongo: no match, and no error. The elements are compared one by one and a + number never matches a regex. + + The array branch keeps only the strings and then aggregates what is left, + and the aggregation rejects an empty array. The scalar branch of the same + operator answers C_FALSE for a number, as this one used to. + */ + Document doc = new Document().append("a", new ArrayList<>(Arrays.asList(1, 2, 3))); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + Document query = new Document().append("a", + new Document().append("$regex", new BsonRegularExpression("x"))); + Truthness truthness = assertDoesNotThrow(() -> calculator.computeHeuristicDocument(query, doc)); + assertTrue(truthness.isFalse()); + } + + @Test + @Disabled("a bitmask held as org.bson.types.Binary is not parsed, so the calculator throws") + public void testBitmaskGivenAsBinaryData() { + /* + mongo: matches, bit 1 of the mask is set and so is bit 1 of the value. + + A bitmask given as binary data is only recognised when it is a byte[]. A + query that has been through a BSON decode holds a Binary instead, which + leaves the mask unparsed, and the whole query then parses to null. + */ + Document doc = new Document().append("a", 2); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + Document query = new Document().append("a", + new Document().append("$bitsAllSet", new Binary(new byte[]{(byte) 0x02}))); + Truthness truthness = assertDoesNotThrow(() -> calculator.computeHeuristicDocument(query, doc)); + assertTrue(truthness.isTrue()); + } + + @Test + public void testBitmaskGivenAsAByteArray() { + // the shape of binary bitmask that is handled today, next to the one above + Document doc = new Document().append("a", 2); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + Document query = new Document().append("a", + new Document().append("$bitsAllSet", new byte[]{(byte) 0x02})); + assertTrue(calculator.computeHeuristicDocument(query, doc).isTrue()); + } + + @Test + @Disabled("$not holding a bare regex is not parsed, so the calculator throws a NullPointerException") + public void testNotWithABareRegex() { + /* + mongo: matches, {"a": {"$not": /x/}} is a documented form of the operator. + Only a document is accepted as the value of $not today, so this parses to + null. + */ + Document doc = new Document().append("a", "aa"); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + Document query = new Document().append("a", + new Document().append("$not", new BsonRegularExpression("x"))); + Truthness truthness = assertDoesNotThrow(() -> calculator.computeHeuristicDocument(query, doc)); + assertTrue(truthness.isTrue()); + } + + @Test + @Disabled("$comment is removed from documents that are values rather than operators") + public void testCommentInsideALiteralValue() { + /* + mongo: matches. A field name that starts with "$" is storable since 5.0, and + under an explicit $eq the document is compared as a literal. The implicit + form, {"a": {"$comment": ...}}, is rejected instead. + + The comment operator is removed everywhere it appears in the query, so the + value being compared loses the key as well and no longer equals the stored + document. + */ + Document doc = new Document().append("a", + new Document().append("$comment", "note").append("x", 1)); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + Document query = new Document().append("a", new Document().append("$eq", + new Document().append("$comment", "note").append("x", 1))); + assertTrue(calculator.computeHeuristicDocument(query, doc).isTrue()); + } + + @Test + public void testCommentAtTopLevel() { + // the placement of $comment that the query is actually allowed to use + Document doc = new Document().append("a", 1); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + Document query = new Document().append("a", 1).append("$comment", "note"); + assertTrue(calculator.computeHeuristicDocument(query, doc).isTrue()); + } + + @Test + @Disabled("$all with a repeated element does not match a field that is not an array") + public void testAllWithARepeatedElement() { + // mongo: matches. $all is an $and of $eq, so a repeat holds against a scalar. + Document doc = new Document().append("a", 1); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + Document query = new Document().append("a", + new Document().append("$all", Arrays.asList(1, 1))); + assertTrue(calculator.computeHeuristicDocument(query, doc).isTrue()); + } + + @Test + @Disabled("$all holding more than one null does not match a missing field") + public void testAllWithRepeatedNullsAgainstAMissingField() { + // mongo: matches, null matches a missing field however many times it is asked for + Document doc = new Document().append("b", 1); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + Document query = new Document().append("a", + new Document().append("$all", Arrays.asList(null, null))); + assertTrue(calculator.computeHeuristicDocument(query, doc).isTrue()); + } + + @Test + public void testAllWithASingleNullAgainstAMissingField() { + // the single element case, which is special-cased today + Document doc = new Document().append("b", 1); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + Document query = new Document().append("a", + new Document().append("$all", Collections.singletonList(null))); + assertTrue(calculator.computeHeuristicDocument(query, doc).isTrue()); + } + + @Test + @Disabled("$in does not match an element of the list that is a regex") + public void testInWithARegexElement() { + // mongo: matches, $in accepts regexes among its elements + Document doc = new Document().append("a", "xy"); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + Document query = new Document().append("a", new Document().append("$in", + Collections.singletonList(new BsonRegularExpression("x")))); + assertTrue(calculator.computeHeuristicDocument(query, doc).isTrue()); + } + + @Test + @Disabled("two sub-documents are never equal to one another, so the query never matches") + public void testEqualityBetweenTwoSubDocuments() { + /* + mongo: matches every one of these. A sub-document is compared field by field + in order, and an array is searched for it as an element. + + compareNonNullValues has a branch for numbers, strings, booleans, lists, + dates, timestamps and object ids, but none for two documents, so they reach + the branch for values of types that cannot be compared and answer false. + testFieldsHoldingASubDocument above covers a sub-document against a scalar, + where answering false is right, which is why this one is not caught there. + */ + Document doc = new Document().append("a", new Document().append("x", 1)); + Document arrayOfSubDocuments = new Document().append("a", new ArrayList<>( + Arrays.asList(new Document().append("x", 1), new Document().append("x", 2)))); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + Document implicitEquality = new Document().append("a", new Document().append("x", 1)); + assertTrue(calculator.computeHeuristicDocument(implicitEquality, doc).isTrue()); + + Document explicitEquality = new Document().append("a", + new Document().append("$eq", new Document().append("x", 1))); + assertTrue(calculator.computeHeuristicDocument(explicitEquality, doc).isTrue()); + + // mongo: matches, the array is searched for the sub-document as one of its elements + assertTrue(calculator.computeHeuristicDocument(implicitEquality, arrayOfSubDocuments).isTrue()); + } + + @Test + @Disabled("$ne and $nin report a match between two sub-documents that are equal") + public void testInequalityBetweenTwoSubDocuments() { + /* + mongo: no match for either, the sub-document held by the field is the one the + query names. + + The same gap as the test above, in the direction that hurts: two documents + reach the branch for values of types that cannot be compared, which answers + true for $ne, and $nin reaches the same answer by inverting it once more. + */ + Document doc = new Document().append("a", new Document().append("x", 1)); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + Document notEquals = new Document().append("a", + new Document().append("$ne", new Document().append("x", 1))); + assertTrue(calculator.computeHeuristicDocument(notEquals, doc).isFalse()); + + Document notIn = new Document().append("a", new Document().append("$nin", + Collections.singletonList(new Document().append("x", 1)))); + assertTrue(calculator.computeHeuristicDocument(notIn, doc).isFalse()); + } + + @Test + @Disabled("$eq does not match an array holding the given array as one of its elements") + public void testEqualsMatchingAnArrayThatIsAnElement() { + /* + mongo: matches, the field holds [1,2] as one of its elements. The other half + of this operator, an array equal to the query value as a whole, is covered by + testEqualsMatchingAnArrayFieldAsAWhole above and already answers correctly. + + $in and $all both find a nested array in this position, so $eq is the one of + the three that does not. + */ + Document doc = new Document().append("a", new ArrayList<>( + Arrays.asList(new ArrayList<>(Arrays.asList(1, 2)), 3))); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + Document query = new Document().append("a", + new Document().append("$eq", Arrays.asList(1, 2))); + assertTrue(calculator.computeHeuristicDocument(query, doc).isTrue()); + } + + @Test + @Disabled("two binary values are never equal to one another, so the query never matches") + public void testComparisonBetweenTwoBinaryValues() { + /* + mongo: the first three match and the last two do not. + + The same gap as testEqualityBetweenTwoSubDocuments, reached by the other value + compareNonNullValues has no branch for. testFieldsHoldingASubDocument above + covers binary data against a number, where answering false is right. + */ + Document doc = new Document().append("a", new Binary(new byte[]{1, 2})); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + assertTrue(calculator.computeHeuristicDocument( + new Document().append("a", new Binary(new byte[]{1, 2})), doc).isTrue()); + assertTrue(calculator.computeHeuristicDocument( + new Document().append("a", new Document().append("$eq", new Binary(new byte[]{1, 2}))), + doc).isTrue()); + assertTrue(calculator.computeHeuristicDocument( + new Document().append("a", new Document().append("$in", + Collections.singletonList(new Binary(new byte[]{1, 2})))), doc).isTrue()); + + // mongo: no match, the value held by the field is the one the query names + assertTrue(calculator.computeHeuristicDocument( + new Document().append("a", new Document().append("$ne", new Binary(new byte[]{1, 2}))), + doc).isFalse()); + assertTrue(calculator.computeHeuristicDocument( + new Document().append("a", new Document().append("$nin", + Collections.singletonList(new Binary(new byte[]{1, 2})))), doc).isFalse()); + } + + @Test + @Disabled("$all holding an $elemMatch is compared as a value instead of applied as a condition") + public void testAllHoldingAnElemMatch() { + /* + mongo: matches both. $all accepts $elemMatch among its elements, and then every + one of them has to be satisfied by some element of the array. + + Each element of $all is compared for equality against the elements of the field + instead, so the document holding the operator never equals any of them. + */ + Document doc = new Document().append("a", new ArrayList<>(Arrays.asList(1, 5))); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + Document one = new Document().append("a", new Document().append("$all", + Collections.singletonList(new Document().append("$elemMatch", + new Document().append("$gt", 2))))); + assertTrue(calculator.computeHeuristicDocument(one, doc).isTrue()); + + // mongo: matches, 5 satisfies the first and 1 satisfies the second + Document two = new Document().append("a", new Document().append("$all", Arrays.asList( + new Document().append("$elemMatch", new Document().append("$gt", 4)), + new Document().append("$elemMatch", new Document().append("$lt", 2))))); + assertTrue(calculator.computeHeuristicDocument(two, doc).isTrue()); + } + + @Test + @Disabled("an ordering operator given null is not parsed, so the calculator throws") + public void testOrderingComparisonsGivenANullArgument() { + /* + mongo: runs all four and matches nothing, null orders against nothing. It is a + query the database accepts, unlike $mod with a zero divisor or a negative $size. + + $eq and $ne accept null and answer, so the four below are the odd ones out in + their own family. + */ + Document doc = new Document().append("a", 1); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + for (String operator : Arrays.asList("$gt", "$gte", "$lt", "$lte")) { + Document query = new Document().append("a", new Document().append(operator, null)); + Truthness truthness = assertDoesNotThrow( + () -> calculator.computeHeuristicDocument(query, doc), operator); + assertTrue(truthness.isFalse(), operator); + } + } + + @Test + @Disabled("$exists given anything but a boolean is not parsed, so the calculator throws") + public void testExistsGivenAValueThatIsNotABoolean() { + /* + mongo: takes any value for $exists and reads it as a truth value, so 1 and a + string ask for the field to be present, and 0 and null ask for it to be absent. + All four are queries the database accepts and answers. + */ + Document doc = new Document().append("a", 1); + Document without = new Document().append("b", 1); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + Document truthy = new Document().append("a", new Document().append("$exists", 1)); + assertTrue(assertDoesNotThrow(() -> calculator.computeHeuristicDocument(truthy, doc)).isTrue()); + + Document falsy = new Document().append("a", new Document().append("$exists", 0)); + assertTrue(assertDoesNotThrow(() -> calculator.computeHeuristicDocument(falsy, doc)).isFalse()); + assertTrue(assertDoesNotThrow(() -> calculator.computeHeuristicDocument(falsy, without)).isTrue()); + + Document nullValue = new Document().append("a", new Document().append("$exists", null)); + assertTrue(assertDoesNotThrow(() -> calculator.computeHeuristicDocument(nullValue, doc)).isFalse()); + } + + @Test + @Disabled("a negative bit position is read as bit 63 instead of being rejected") + public void testBitmaskWithANegativeBitPosition() { + /* + mongo: "Failed to parse bit position. Expected a non-negative number in: 0: -1". + + A shift count is masked to its low six bits in Java, so 1L << -1 sets bit 63 + and the mask becomes Long.MIN_VALUE, which the value below then matches. + Rejecting the mask on its own is not enough here: if the mask stops parsing, + the selector answers null and the query goes unparsed, which is the throwing + case rather than a fix. + */ + Document doc = new Document().append("a", Long.MIN_VALUE); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + Document query = new Document().append("a", + new Document().append("$bitsAllSet", Collections.singletonList(-1))); + Truthness truthness = assertDoesNotThrow(() -> calculator.computeHeuristicDocument(query, doc)); + assertTrue(truthness.isFalse()); + } + + @Test + @Disabled("a bitmask that is not an integer is truncated instead of being rejected") + public void testBitmaskThatIsNotAnInteger() { + // mongo: "Expected an integer: $bitsAllSet: 3.9". Here 3.9 is read as the mask 3. + Document doc = new Document().append("a", 3); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + Document query = new Document().append("a", new Document().append("$bitsAllSet", 3.9d)); + Truthness truthness = assertDoesNotThrow(() -> calculator.computeHeuristicDocument(query, doc)); + assertTrue(truthness.isFalse()); + } + + @Test + @Disabled("a negative bitmask is read as all ones instead of being rejected") + public void testBitmaskThatIsNegative() { + // mongo: "Expected a non-negative number in: $bitsAllSet: -1" + Document doc = new Document().append("a", -1); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + Document query = new Document().append("a", new Document().append("$bitsAllSet", -1)); + Truthness truthness = assertDoesNotThrow(() -> calculator.computeHeuristicDocument(query, doc)); + assertTrue(truthness.isFalse()); + } + + @Test + @Disabled("$mod with a divisor of 0 throws ArithmeticException") + public void testModByZero() { + // mongo: "divisor cannot be 0" + Document doc = new Document().append("a", 5); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + Document query = new Document().append("a", + new Document().append("$mod", Arrays.asList(0, 0))); + Truthness truthness = assertDoesNotThrow(() -> calculator.computeHeuristicDocument(query, doc)); + assertTrue(truthness.isFalse()); + } + + @Test + @Disabled("$size with a negative value is not parsed, so the calculator throws a NullPointerException") + public void testNegativeSize() { + // mongo: "Expected a non-negative number in: $size: -1" + Document doc = new Document().append("a", Arrays.asList(1, 2)); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + Document query = new Document().append("a", new Document().append("$size", -1)); + Truthness truthness = assertDoesNotThrow(() -> calculator.computeHeuristicDocument(query, doc)); + assertTrue(truthness.isFalse()); + } + + @Test + @Disabled("the documents are traversed more than once, so a one-shot Iterable is scored wrongly") + public void testDocumentsThatCanOnlyBeTraversedOnce() { + /* + Not a question of MongoDB semantics but of the contract of the calculator. + + computeDistanceDocuments counts the documents, computeHeuristicOnDocuments + counts them again, and the loop then walks them a third time. What is passed + in today is a FindIterable, which replays, so this costs a round trip to the + database per traversal rather than a wrong answer. The signature accepts any + Iterable though, and one that cannot be replayed is scored as if the + collection were empty while still reporting the documents counted first. + */ + List docs = Arrays.asList( + new Document().append("a", 1), + new Document().append("a", 2)); + Document query = convertToDocument(Filters.eq("a", 2)); + + // the second document matches the query, so the distance is 0 over either Iterable + MongoDistanceWithMetrics fromList = new MongoHeuristicsCalculator() + .computeDistanceDocuments(query, docs); + assertEquals(2, fromList.numberOfEvaluatedDocuments); + assertEquals(0d, fromList.mongoDistance, 0.000001d); + + MongoDistanceWithMetrics fromOneShot = new MongoHeuristicsCalculator() + .computeDistanceDocuments(query, new OneShotIterable<>(docs)); + assertEquals(2, fromOneShot.numberOfEvaluatedDocuments); + assertEquals(0d, fromOneShot.mongoDistance, 0.000001d); + } + + /** + * An Iterable that can be traversed only once. + */ + private static final class OneShotIterable implements Iterable { + + private final Iterable delegate; + private boolean consumed = false; + + private OneShotIterable(Iterable delegate) { + this.delegate = delegate; + } + + @Override + public Iterator iterator() { + if (consumed) { + return Collections.emptyIterator(); + } + consumed = true; + return delegate.iterator(); + } + } } From bba0b74cdde3a39792863e991324c8e912755817 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Thu, 17 Sep 2026 19:05:01 -0300 Subject: [PATCH 22/53] $mod with zero divisor is not accepted as a valid MongoDB query anymore. --- .../mongo/selectors/ModSelector.java | 3 +++ .../mongo/MongoHeuristicsCalculatorTest.java | 25 ------------------- .../controller/mongo/QueryParserTest.java | 10 ++++++++ 3 files changed, 13 insertions(+), 25 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ModSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ModSelector.java index c127cb6f13..828e188b1d 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ModSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ModSelector.java @@ -26,6 +26,9 @@ protected QueryOperation parseValue(String fieldName, Object value) { } Long divisor = ((Number) listOfValues.get(0)).longValue(); Long remainder = ((Number) listOfValues.get(1)).longValue(); + if (divisor == 0) { + return null; // divisor cannot be zero + } return new ModOperation(fieldName, divisor, remainder); } else { return null; diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 80373e8f8b..f24e14c174 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -2489,31 +2489,6 @@ public void testBitmaskThatIsNegative() { assertTrue(truthness.isFalse()); } - @Test - @Disabled("$mod with a divisor of 0 throws ArithmeticException") - public void testModByZero() { - // mongo: "divisor cannot be 0" - Document doc = new Document().append("a", 5); - MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); - - Document query = new Document().append("a", - new Document().append("$mod", Arrays.asList(0, 0))); - Truthness truthness = assertDoesNotThrow(() -> calculator.computeHeuristicDocument(query, doc)); - assertTrue(truthness.isFalse()); - } - - @Test - @Disabled("$size with a negative value is not parsed, so the calculator throws a NullPointerException") - public void testNegativeSize() { - // mongo: "Expected a non-negative number in: $size: -1" - Document doc = new Document().append("a", Arrays.asList(1, 2)); - MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); - - Document query = new Document().append("a", new Document().append("$size", -1)); - Truthness truthness = assertDoesNotThrow(() -> calculator.computeHeuristicDocument(query, doc)); - assertTrue(truthness.isFalse()); - } - @Test @Disabled("the documents are traversed more than once, so a one-shot Iterable is scored wrongly") public void testDocumentsThatCanOnlyBeTraversedOnce() { diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java index 6781601faf..4cb0f600b9 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java @@ -554,6 +554,16 @@ void testParseMod() { assertEquals(0L, mod.getRemainder()); } + @Test + void testModRejectZeroDivisor() { + Document query = new Document( + "age", + new Document("$mod", Arrays.asList(0L, 0L)) + ); + QueryOperation operation = parser.parse(query); + assertNull(operation); + } + @Test void testParseBitsAllClearLong() { Document query = new Document( From 8791c29fa74f899c77e39aa5fc0f6da2310cade7 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Thu, 17 Sep 2026 19:08:57 -0300 Subject: [PATCH 23/53] evaluate $regex operation vs. an array checks that at least one element of the array is a string. Otherwise returns C_FALSE. --- .../mongo/MongoHeuristicsCalculator.java | 16 ++++++++++------ .../mongo/MongoHeuristicsCalculatorTest.java | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java index 521dcf2008..b41688aeca 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java @@ -209,12 +209,16 @@ private Truthness evaluate(RegexOperation operation, Object actualValue) { if (actualValueList.isEmpty()) { return C_FALSE; } else { - Truthness[] results = actualValueList.stream() - .filter(element -> element instanceof String) - .map(element -> (String) element) - .map(element -> evaluateRegularExpression(element, pattern, taintHandler)) - .toArray(Truthness[]::new); - return buildOrAggregationTruthness(results); + if (actualValueList.stream().anyMatch(element -> !(element instanceof String))) { + return C_FALSE; + } else { + Truthness[] results = actualValueList.stream() + .filter(element -> element instanceof String) + .map(element -> (String) element) + .map(element -> evaluateRegularExpression(element, pattern, taintHandler)) + .toArray(Truthness[]::new); + return buildOrAggregationTruthness(results); + } } } else { return C_FALSE; diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index f24e14c174..688e459ca7 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -2128,7 +2128,7 @@ public void testNotWithMoreThanOneInnerOperator() { */ @Test - @Disabled("$regex against an array holding no strings throws IllegalArgumentException") + //@Disabled("$regex against an array holding no strings throws IllegalArgumentException") public void testRegexAgainstAnArrayWithoutStringElements() { /* mongo: no match, and no error. The elements are compared one by one and a From 3217b2f37ede7cc6e2532d16ca3734efa0408930 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Thu, 17 Sep 2026 19:09:49 -0300 Subject: [PATCH 24/53] remove //@Disabled --- .../java/controller/mongo/MongoHeuristicsCalculatorTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 688e459ca7..c6b17cd8ea 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -2128,7 +2128,6 @@ public void testNotWithMoreThanOneInnerOperator() { */ @Test - //@Disabled("$regex against an array holding no strings throws IllegalArgumentException") public void testRegexAgainstAnArrayWithoutStringElements() { /* mongo: no match, and no error. The elements are compared one by one and a From f48d59f816e128de5d887ccc273e4a80a840d3f8 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Thu, 17 Sep 2026 20:03:20 -0300 Subject: [PATCH 25/53] $eq checks for equality of List or elemens when comparing two lists. --- .../mongo/MongoHeuristicsCalculator.java | 26 ++++++++++++------- .../mongo/MongoHeuristicsCalculatorTest.java | 16 ++++++++++-- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java index b41688aeca..caa0ff7b84 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java @@ -272,15 +272,24 @@ private Truthness computeHeuristic(EmptyOperation operation, Object document) { */ private Truthness evaluate(EqualsOperation operation, Object actualValue) { Objects.requireNonNull(operation); - Object expectedValue = operation.getValue(); - if ((actualValue instanceof List) && !(expectedValue instanceof List)) { - return helper.computeHeuristicContainsElement(expectedValue, (List) actualValue); - } else { - return helper.compareNullableValues( - expectedValue, - SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO, actualValue - ); + final Object expectedValue = operation.getValue(); + + if ((actualValue instanceof List) && (expectedValue instanceof List)) { + List actualList = (List) actualValue; + List expectedList = (List) expectedValue; + Truthness wholeListComparison = helper.compareNullableValues(actualList, SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO, expectedList); + if (wholeListComparison.isTrue()) { + return wholeListComparison; + } } + + return evaluateWithArrayUnwrapping(actualValue, + value -> helper.compareNullableValues( + expectedValue, + SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO, value + )); + + } @@ -336,7 +345,6 @@ private Truthness evaluate(GreaterThanEqualsOperation operation, Object actua expectedValue, SqlExpressionEvaluator.ComparisonOperatorType.GREATER_THAN_EQUALS, value )); - } private Truthness evaluate(LessThanOperation operation, Object actualValue) { diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index c6b17cd8ea..d331c76952 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -33,7 +33,7 @@ public class MongoHeuristicsCalculatorTest { @Test public void testEqualsScalarToList() { - Document doc = new Document().append("tags", Arrays.asList("red","blue")); + Document doc = new Document().append("tags", Arrays.asList("red", "blue")); Bson bsonTrue = Filters.eq("tags", "red"); Bson bsonFalse = Filters.eq("tags", "green"); Truthness distanceMatch = new MongoHeuristicsCalculator().computeHeuristicDocument(convertToDocument(bsonTrue), doc); @@ -1118,7 +1118,7 @@ public void testEqualsLists() { public void testNotEqualsLists() { Document doc = new Document().append("employees", Arrays.asList("Alice")); Bson bsonTrue = Filters.ne("employees", Arrays.asList("Alice", "Bob")); - Bson bsonFalse = Filters.ne ("employees", Arrays.asList("Alice")); + Bson bsonFalse = Filters.ne("employees", Arrays.asList("Alice")); Truthness distanceMatch = new MongoHeuristicsCalculator().computeHeuristicDocument(convertToDocument(bsonTrue), doc); Truthness distanceNotMatch = new MongoHeuristicsCalculator().computeHeuristicDocument(convertToDocument(bsonFalse), doc); assertTrue(distanceMatch.isTrue()); @@ -1947,6 +1947,18 @@ public void testDottedFieldPath() { new Document().append("x", 1), new Document().append("x", 2))))).isTrue()); } + @Test + public void testEqualityWithListOfLists() { + Document doc = new Document().append("a", Arrays.asList(Arrays.asList(1, 2))); + + Document query = new Document().append("a", new Document().append("$eq", Arrays.asList(1, 2))); + + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + Truthness score = calculator.computeHeuristicDocument(query, doc); + + assertTrue(score.isTrue()); + } + /* ================================================================================ Cases about the BSON value types a field can hold, and about the operators that From e9ce2356c95659e64f5a723ceb0930e78bbf8008 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Thu, 17 Sep 2026 21:08:51 -0300 Subject: [PATCH 26/53] $all semantics is re-implemented as an $and of $eq operations. --- .../mongo/MongoHeuristicsCalculator.java | 94 ++++--------------- .../MongoHeuristicsCalculatorHelper.java | 42 +++++++++ .../mongo/MongoHeuristicsCalculatorTest.java | 11 +-- 3 files changed, 63 insertions(+), 84 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java index caa0ff7b84..92b99423e9 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java @@ -273,23 +273,7 @@ private Truthness computeHeuristic(EmptyOperation operation, Object document) { private Truthness evaluate(EqualsOperation operation, Object actualValue) { Objects.requireNonNull(operation); final Object expectedValue = operation.getValue(); - - if ((actualValue instanceof List) && (expectedValue instanceof List)) { - List actualList = (List) actualValue; - List expectedList = (List) expectedValue; - Truthness wholeListComparison = helper.compareNullableValues(actualList, SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO, expectedList); - if (wholeListComparison.isTrue()) { - return wholeListComparison; - } - } - - return evaluateWithArrayUnwrapping(actualValue, - value -> helper.compareNullableValues( - expectedValue, - SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO, value - )); - - + return helper.evaluateEquality(actualValue, expectedValue); } @@ -328,7 +312,7 @@ private Truthness evaluate(GreaterThanOperation operation, Object actualValue Objects.requireNonNull(operation); Object expectedValue = operation.getValue(); - return evaluateWithArrayUnwrapping(actualValue, + return helper.evaluateWithArrayUnwrapping(actualValue, value -> helper.compareNullableValues( expectedValue, SqlExpressionEvaluator.ComparisonOperatorType.GREATER_THAN, value @@ -340,7 +324,7 @@ private Truthness evaluate(GreaterThanEqualsOperation operation, Object actua Objects.requireNonNull(operation); final Object expectedValue = operation.getValue(); - return evaluateWithArrayUnwrapping(actualValue, + return helper.evaluateWithArrayUnwrapping(actualValue, value -> helper.compareNullableValues( expectedValue, SqlExpressionEvaluator.ComparisonOperatorType.GREATER_THAN_EQUALS, value @@ -351,7 +335,7 @@ private Truthness evaluate(LessThanOperation operation, Object actualValue) { Objects.requireNonNull(operation); final Object expectedValue = operation.getValue(); - return evaluateWithArrayUnwrapping(actualValue, + return helper.evaluateWithArrayUnwrapping(actualValue, value -> helper.compareNullableValues( expectedValue, SqlExpressionEvaluator.ComparisonOperatorType.MINOR_THAN, value @@ -362,41 +346,13 @@ private Truthness evaluate(LessThanEqualsOperation operation, Object actualVa Objects.requireNonNull(operation); final Object expectedValue = operation.getValue(); - return evaluateWithArrayUnwrapping(actualValue, + return helper.evaluateWithArrayUnwrapping(actualValue, value -> helper.compareNullableValues( expectedValue, SqlExpressionEvaluator.ComparisonOperatorType.MINOR_THAN_EQUALS, value )); } - /** - * Evaluates a nested structure or a single value using a provided heuristic function. - * For nested structures that are lists, it aggregates the results of applying the heuristic - * function to each element in the list. For single values, it directly applies the heuristic. - * - * @param value the value to evaluate, which can either be a single value or a list of values - * @param elementHeuristic a function to compute the heuristic for each element or the single value - * @return a Truthness object representing the heuristic of the evaluated input - */ - private Truthness evaluateWithArrayUnwrapping(Object value, - Function elementHeuristic) { - Objects.requireNonNull(elementHeuristic); - - if (!(value instanceof List)) { - // value is not an array. Inspect only this value. - return elementHeuristic.apply(value); - } else { - // value is an array. Inspect all elements. - List values = (List) value; - if (values.isEmpty()) { - return C_FALSE; - } - Truthness orAggregation = buildOrAggregationTruthness(values.stream() - .map(elementHeuristic) - .toArray(Truthness[]::new)); - return buildSafeScaledTruthness(orAggregation); - } - } private Truthness computeHeuristic(OrOperation operation, Object document) { Objects.requireNonNull(operation); @@ -442,32 +398,14 @@ private Truthness evaluate(AllOperation operation, Object actualValue) { return C_FALSE; } - if (actualValue == null) { - return (expectedValues.size() == 1 && expectedValues.get(0) == null) - ? TRUE_C - : C_FALSE; - } - - if (!(actualValue instanceof List)) { - if (expectedValues.size() != 1) { - return C_FALSE; - } else { - return helper.compareNullableValues( - expectedValues.get(0), - SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO, actualValue - ); - } - } + Truthness truthness = buildAndAggregationTruthness( + expectedValues.stream() + .map(expectedElementValue -> + helper.evaluateEquality(actualValue,expectedElementValue)) + .toArray(Truthness[]::new)); - List actualValues = (List) actualValue; - if (actualValues.isEmpty()) { - return C_FALSE; - } + return truthness; - Truthness res = buildAndAggregationTruthness(expectedValues.stream() - .map(expectedValue -> helper.computeHeuristicContainsElement(expectedValue, actualValues)) - .toArray(Truthness[]::new)); - return buildSafeScaledTruthness(res); } private Truthness evaluate(SizeOperation operation, Object actualValue) { @@ -570,31 +508,31 @@ private Truthness evaluate(ModOperation operation, Object actualValue) { long divisor = operation.getDivisor().longValue(); long expectedRemainder = operation.getRemainder().longValue(); - return evaluateWithArrayUnwrapping(actualValue, + return helper.evaluateWithArrayUnwrapping(actualValue, value -> helper.evaluateMod(divisor, expectedRemainder, value)); } private Truthness evaluate(BitsAllClearOperation operation, Object actualValue) { Objects.requireNonNull(operation); - return evaluateWithArrayUnwrapping(actualValue, + return helper.evaluateWithArrayUnwrapping(actualValue, value -> helper.evaluateBitsAllClearOperation(operation.getBitmask(), value)); } private Truthness evaluate(BitsAnyClearOperation operation, Object actualValue) { Objects.requireNonNull(operation); - return evaluateWithArrayUnwrapping(actualValue, + return helper.evaluateWithArrayUnwrapping(actualValue, value -> helper.evaluateBitsAnyClearOperation(operation.getBitmask(), value)); } private Truthness evaluate(BitsAllSetOperation operation, Object actualValue) { Objects.requireNonNull(operation); - return evaluateWithArrayUnwrapping(actualValue, + return helper.evaluateWithArrayUnwrapping(actualValue, value -> helper.evaluateBitsAllSetOperation(operation.getBitmask(), value)); } private Truthness evaluate(BitsAnySetOperation operation, Object actualValue) { Objects.requireNonNull(operation); - return evaluateWithArrayUnwrapping(actualValue, + return helper.evaluateWithArrayUnwrapping(actualValue, value -> helper.evaluateBitsAnySetOperation(operation.getBitmask(), value)); } diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java index 34019eb3c8..5aedfe95ed 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java @@ -13,6 +13,7 @@ import java.util.List; import java.util.Objects; import java.util.OptionalLong; +import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -437,4 +438,45 @@ static Truthness evaluateDistanceBetweenPoints(double minDistance, : getEqualityTruthness(distanceBetweenPoints, minDistance); } + Truthness evaluateEquality(Object actualValue, Object expectedValue) { + Truthness topLevelTruthness = this.compareNullableValues(actualValue, SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO, expectedValue); + if (topLevelTruthness.isTrue()) { + return topLevelTruthness; + } else { + return evaluateWithArrayUnwrapping(actualValue, + value -> this.compareNullableValues( + expectedValue, + SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO, value)); + } + } + + /** + * Evaluates a nested structure or a single value using a provided heuristic function. + * For nested structures that are lists, it aggregates the results of applying the heuristic + * function to each element in the list. For single values, it directly applies the heuristic. + * + * @param value the value to evaluate, which can either be a single value or a list of values + * @param elementHeuristic a function to compute the heuristic for each element or the single value + * @return a Truthness object representing the heuristic of the evaluated input + */ + Truthness evaluateWithArrayUnwrapping(Object value, + Function elementHeuristic) { + Objects.requireNonNull(elementHeuristic); + + if (!(value instanceof List)) { + // value is not an array. Inspect only this value. + return elementHeuristic.apply(value); + } else { + // value is an array. Inspect all elements. + List values = (List) value; + if (values.isEmpty()) { + return C_FALSE; + } + Truthness orAggregation = buildOrAggregationTruthness(values.stream() + .map(elementHeuristic) + .toArray(Truthness[]::new)); + return buildSafeScaledTruthness(orAggregation); + } + } + } diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index d331c76952..9d96fbc679 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -274,9 +274,9 @@ public void testNotInMissingField() { @Test public void testAll() { - Document doc = new Document().append("employees", new ArrayList<>(Arrays.asList(1, 5, 6))); - Bson bsonTrue = Filters.all("employees", new ArrayList<>(Arrays.asList(1, 5, 6))); - Bson bsonFalse = Filters.all("employees", new ArrayList<>(Arrays.asList(1, 7, 8))); + Document doc = new Document().append("employees", Arrays.asList(1, 5, 6)); + Bson bsonTrue = Filters.all("employees", Arrays.asList(1, 5, 6)); + Bson bsonFalse = Filters.all("employees", Arrays.asList(1, 7, 8)); Truthness distanceMatch = new MongoHeuristicsCalculator().computeHeuristicDocument(convertToDocument(bsonTrue), doc); Truthness distanceNotMatch = new MongoHeuristicsCalculator().computeHeuristicDocument(convertToDocument(bsonFalse), doc); assertTrue(distanceMatch.isTrue()); @@ -2237,14 +2237,13 @@ public void testCommentAtTopLevel() { } @Test - @Disabled("$all with a repeated element does not match a field that is not an array") public void testAllWithARepeatedElement() { // mongo: matches. $all is an $and of $eq, so a repeat holds against a scalar. Document doc = new Document().append("a", 1); - MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); - Document query = new Document().append("a", new Document().append("$all", Arrays.asList(1, 1))); + + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); assertTrue(calculator.computeHeuristicDocument(query, doc).isTrue()); } From 49344bedd4c375ef4a206f8fcb737dbd2b9196ba Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Thu, 17 Sep 2026 21:27:59 -0300 Subject: [PATCH 27/53] refactor: left/right renamed to expected/actual --- .../mongo/MongoHeuristicsCalculator.java | 32 ++-- .../MongoHeuristicsCalculatorHelper.java | 162 +++++++++--------- 2 files changed, 94 insertions(+), 100 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java index 92b99423e9..47ae8f2f9f 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java @@ -11,9 +11,9 @@ import static org.evomaster.client.java.controller.mongo.utils.MongoUtils.*; import static org.evomaster.client.java.controller.mongo.utils.MongoUtils.GeoSpatialModel.SPHERICAL; import static org.evomaster.client.java.distance.heuristics.TruthnessUtils.*; +import static org.evomaster.client.java.sql.heuristic.SqlExpressionEvaluator.ComparisonOperatorType.*; import java.util.*; -import java.util.function.Function; import java.util.regex.Pattern; import java.util.stream.StreamSupport; @@ -202,8 +202,8 @@ private Truthness evaluate(RegexOperation operation, Object actualValue) { if (actualValue == null) { return C_FALSE; } else if (actualValue instanceof String) { - final String inputValue = (String) actualValue; - return evaluateRegularExpression(inputValue, pattern, taintHandler); + final String actualValueAsString = (String) actualValue; + return evaluateRegularExpression(actualValueAsString, pattern, taintHandler); } else if (actualValue instanceof List) { List actualValueList = (List) actualValue; if (actualValueList.isEmpty()) { @@ -302,7 +302,8 @@ private Truthness evaluate(NotEqualsOperation operation, Object actualValue) } else { return helper.compareNullableValues( expectedValue, - SqlExpressionEvaluator.ComparisonOperatorType.NOT_EQUALS_TO, actualValue + NOT_EQUALS_TO, + actualValue ); } } @@ -315,7 +316,8 @@ private Truthness evaluate(GreaterThanOperation operation, Object actualValue return helper.evaluateWithArrayUnwrapping(actualValue, value -> helper.compareNullableValues( expectedValue, - SqlExpressionEvaluator.ComparisonOperatorType.GREATER_THAN, value + GREATER_THAN, + value )); } @@ -325,10 +327,7 @@ private Truthness evaluate(GreaterThanEqualsOperation operation, Object actua final Object expectedValue = operation.getValue(); return helper.evaluateWithArrayUnwrapping(actualValue, - value -> helper.compareNullableValues( - expectedValue, - SqlExpressionEvaluator.ComparisonOperatorType.GREATER_THAN_EQUALS, value - )); + value -> helper.compareNullableValues(expectedValue, GREATER_THAN_EQUALS, value)); } private Truthness evaluate(LessThanOperation operation, Object actualValue) { @@ -336,10 +335,7 @@ private Truthness evaluate(LessThanOperation operation, Object actualValue) { final Object expectedValue = operation.getValue(); return helper.evaluateWithArrayUnwrapping(actualValue, - value -> helper.compareNullableValues( - expectedValue, - SqlExpressionEvaluator.ComparisonOperatorType.MINOR_THAN, value - )); + value -> helper.compareNullableValues(expectedValue, MINOR_THAN, value)); } private Truthness evaluate(LessThanEqualsOperation operation, Object actualValue) { @@ -347,9 +343,7 @@ private Truthness evaluate(LessThanEqualsOperation operation, Object actualVa final Object expectedValue = operation.getValue(); return helper.evaluateWithArrayUnwrapping(actualValue, - value -> helper.compareNullableValues( - expectedValue, - SqlExpressionEvaluator.ComparisonOperatorType.MINOR_THAN_EQUALS, value + value -> helper.compareNullableValues(expectedValue, MINOR_THAN_EQUALS, value )); } @@ -401,7 +395,7 @@ private Truthness evaluate(AllOperation operation, Object actualValue) { Truthness truthness = buildAndAggregationTruthness( expectedValues.stream() .map(expectedElementValue -> - helper.evaluateEquality(actualValue,expectedElementValue)) + helper.evaluateEquality(actualValue, expectedElementValue)) .toArray(Truthness[]::new)); return truthness; @@ -479,9 +473,7 @@ private Truthness computeHeuristic(ExistsOperation operation, Object input) { } else { Truthness orTruthness = buildOrAggregationTruthness(actualFieldNames.stream() .map(actualFieldName -> - helper.compareNonNullValues(actualFieldName, - SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO, expectedFieldName - )) + helper.compareNonNullValues(expectedFieldName, EQUALS_TO, actualFieldName)) .toArray(Truthness[]::new)); res = buildSafeScaledTruthness(orTruthness); } diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java index 5aedfe95ed..db785170b3 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java @@ -23,6 +23,9 @@ import static org.evomaster.client.java.controller.mongo.utils.MongoUtils.getIntegralLongValue; import static org.evomaster.client.java.distance.heuristics.TruthnessUtils.*; import static org.evomaster.client.java.sql.heuristic.ConversionHelper.convertToInstant; +import static org.evomaster.client.java.sql.heuristic.SqlExpressionEvaluator.ComparisonOperatorType; +import static org.evomaster.client.java.sql.heuristic.SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO; +import static org.evomaster.client.java.sql.heuristic.SqlExpressionEvaluator.ComparisonOperatorType.NOT_EQUALS_TO; public class MongoHeuristicsCalculatorHelper { @@ -54,7 +57,7 @@ static Truthness buildSafeScaledTruthness(double maxOfTrue) { * Optionally, a {@link TaintHandler} can be used to handle tainting information * related to the regular expression processing. * - * @param inputValue the input string to be matched against the regular expression + * @param actualValue the input string to be matched against the regular expression * @param pattern the compiled {@link Pattern} representing the regular expression to match * @param taintHandler an optional implementation of {@link TaintHandler} to handle taint-related processing; * may be null if taint handling is not required @@ -62,25 +65,25 @@ static Truthness buildSafeScaledTruthness(double maxOfTrue) { * where one component (true or false) is 1 to reflect the match status, and the other * captures the approximation in cases of non-exact matches */ - static Truthness evaluateRegularExpression(String inputValue, Pattern pattern, TaintHandler taintHandler) { + static Truthness evaluateRegularExpression(String actualValue, Pattern pattern, TaintHandler taintHandler) { final String patternString = pattern.pattern(); if (taintHandler != null) { final int patternFlags = pattern.flags(); // TODO: tainting should take into account the pattern flags, which can change the matching behavior // TODO: regex could be a partial word match (MongoDB $regex) instead of a whole word match (Matcher.matches()) - taintHandler.handleTaintForRegex(inputValue, patternString); + taintHandler.handleTaintForRegex(actualValue, patternString); } - Matcher matcher = pattern.matcher(inputValue); + Matcher matcher = pattern.matcher(actualValue); boolean matches = matcher.find(); if (matches) { return TRUE_C; } else { // TODO this does not take into account pattern flags, which can change the matching behavior - final int distance = RegexDistanceUtils.getStandardDistance(inputValue, patternString); + final int distance = RegexDistanceUtils.getStandardDistance(actualValue, patternString); // The distance approximation can be zero even when Java's matcher rejects the input // (for example, when flags affect line terminators). Keep non-matches strictly false. double ofTrue = 1d / (1.1d + distance); @@ -98,11 +101,11 @@ static Truthness buildSafeScaledTruthness(Truthness truthness) { return buildSafeScaledTruthness(truthness.getOfTrue()); } - Truthness compareNonNullLists(List leftList, SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType, List rightList) { - Objects.requireNonNull(leftList); - Objects.requireNonNull(rightList); + Truthness compareNonNullLists(List expectedValueAsList, ComparisonOperatorType comparisonOperatorType, List actualValueAsList) { + Objects.requireNonNull(expectedValueAsList); + Objects.requireNonNull(actualValueAsList); - final Truthness truthness = evaluateListEquality(leftList, rightList); + final Truthness truthness = evaluateListEquality(expectedValueAsList, actualValueAsList); switch (comparisonOperatorType) { case EQUALS_TO: return truthness; @@ -125,58 +128,55 @@ Truthness evaluateListEquality(List actualList, List expectedList) { Truthness[] arrayOfTruthnesses = new Truthness[actualList.size()]; for (int i = 0; i < actualList.size(); i++) { - arrayOfTruthnesses[i] = compareNullableValues( - actualList.get(i), - SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO, expectedList.get(i) - ); + arrayOfTruthnesses[i] = compareNullableValues(actualList.get(i), EQUALS_TO, expectedList.get(i)); } Truthness unscaledTruthness = buildAndAggregationTruthness(arrayOfTruthnesses); final Truthness truthness = buildSafeScaledTruthness(unscaledTruthness); return truthness; } - Truthness compareNonNullValues(Object leftValue, + Truthness compareNonNullValues(Object expectedValue, SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType, - Object rightValue) { - Objects.requireNonNull(leftValue); - Objects.requireNonNull(rightValue); + Object actualValue) { + Objects.requireNonNull(expectedValue); + Objects.requireNonNull(actualValue); final Truthness truthnessOfComparison; - if (leftValue instanceof Number && rightValue instanceof Number) { - final Number actualNumber = (Number) leftValue; - final Number expectedNumber = (Number) rightValue; + if (expectedValue instanceof Number && actualValue instanceof Number) { + final Number actualNumber = (Number) expectedValue; + final Number expectedNumber = (Number) actualValue; truthnessOfComparison = compareNumberValues(actualNumber, comparisonOperatorType, expectedNumber); - } else if (leftValue instanceof String && rightValue instanceof String) { - String actualString = (String) leftValue; - String expectedString = (String) rightValue; - if (taintHandler != null && comparisonOperatorType == SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO) { + } else if (expectedValue instanceof String && actualValue instanceof String) { + String actualString = (String) expectedValue; + String expectedString = (String) actualValue; + if (taintHandler != null && comparisonOperatorType == EQUALS_TO) { taintHandler.handleTaintForStringEquals(actualString, expectedString, false); } truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForStringComparison(actualString, expectedString, comparisonOperatorType); - } else if (leftValue instanceof Boolean && rightValue instanceof Boolean) { - int actualIntValue = toIntValue((Boolean) leftValue); - int expectedIntValue = toIntValue((Boolean) rightValue); + } else if (expectedValue instanceof Boolean && actualValue instanceof Boolean) { + int actualIntValue = toIntValue((Boolean) expectedValue); + int expectedIntValue = toIntValue((Boolean) actualValue); truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForNumberComparison( actualIntValue, expectedIntValue, comparisonOperatorType); - } else if (leftValue instanceof List && rightValue instanceof List) { - truthnessOfComparison = compareNonNullLists((List) leftValue, comparisonOperatorType, (List) rightValue); + } else if (expectedValue instanceof List && actualValue instanceof List) { + truthnessOfComparison = compareNonNullLists((List) expectedValue, comparisonOperatorType, (List) actualValue); - } else if (leftValue instanceof Date && rightValue instanceof Date) { - truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForInstantComparison(convertToInstant(leftValue), convertToInstant(rightValue), comparisonOperatorType); + } else if (expectedValue instanceof Date && actualValue instanceof Date) { + truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForInstantComparison(convertToInstant(expectedValue), convertToInstant(actualValue), comparisonOperatorType); - } else if (BsonHelper.isBsonTimestamp(leftValue) && BsonHelper.isBsonTimestamp(rightValue)) { - long actualTimestamp = BsonHelper.getBsonTimestampValue(leftValue); - long expectedTimestamp = BsonHelper.getBsonTimestampValue(rightValue); + } else if (BsonHelper.isBsonTimestamp(expectedValue) && BsonHelper.isBsonTimestamp(actualValue)) { + long actualTimestamp = BsonHelper.getBsonTimestampValue(expectedValue); + long expectedTimestamp = BsonHelper.getBsonTimestampValue(actualValue); truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForNumberComparison(actualTimestamp, expectedTimestamp, comparisonOperatorType); - } else if (BsonHelper.isObjectId(leftValue) || BsonHelper.isObjectId(rightValue)) { - String actualString = leftValue.toString(); - String expectedString = rightValue.toString(); - if (taintHandler != null && comparisonOperatorType == SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO) { + } else if (BsonHelper.isObjectId(expectedValue) || BsonHelper.isObjectId(actualValue)) { + String actualString = expectedValue.toString(); + String expectedString = actualValue.toString(); + if (taintHandler != null && comparisonOperatorType == EQUALS_TO) { taintHandler.handleTaintForStringEquals(actualString, expectedString, false); } truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForStringComparison(actualString, expectedString, comparisonOperatorType); @@ -185,28 +185,28 @@ Truthness compareNonNullValues(Object leftValue, // If both types are supported, but no actual comparison logic is defined, // we considered them to be incompatible, therefore the comparison returns true // only if the comparison operator is NOT_EQUALS_TO. Otherwise returns false. - truthnessOfComparison = comparisonOperatorType == SqlExpressionEvaluator.ComparisonOperatorType.NOT_EQUALS_TO ? TRUE_C : C_FALSE; + truthnessOfComparison = comparisonOperatorType == NOT_EQUALS_TO ? TRUE_C : C_FALSE; } return truthnessOfComparison; } - static Truthness compareNumberValues(Number leftNumber, SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType, Number rightNumber) { - Objects.requireNonNull(leftNumber); - Objects.requireNonNull(rightNumber); + static Truthness compareNumberValues(Number expectedValueAsNumber, SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType, Number actualValueAsNumber) { + Objects.requireNonNull(expectedValueAsNumber); + Objects.requireNonNull(actualValueAsNumber); Objects.requireNonNull(comparisonOperatorType); - double leftValueAsDouble = leftNumber.doubleValue(); - double rightValueAsDouble = rightNumber.doubleValue(); + double expectedValueAsDouble = expectedValueAsNumber.doubleValue(); + double actualValueAsDouble = actualValueAsNumber.doubleValue(); - if (Double.isNaN(leftValueAsDouble) || Double.isNaN(rightValueAsDouble)) { + if (Double.isNaN(expectedValueAsDouble) || Double.isNaN(actualValueAsDouble)) { // handle case when NaN is involved in the comparison switch (comparisonOperatorType) { case EQUALS_TO: { - return (Double.isNaN(leftValueAsDouble) && Double.isNaN(rightValueAsDouble)) ? + return (Double.isNaN(expectedValueAsDouble) && Double.isNaN(actualValueAsDouble)) ? TRUE_C : C_FALSE; } case NOT_EQUALS_TO: { - return (!Double.isNaN(leftValueAsDouble) || !Double.isNaN(rightValueAsDouble)) ? + return (!Double.isNaN(expectedValueAsDouble) || !Double.isNaN(actualValueAsDouble)) ? TRUE_C : C_FALSE; } case GREATER_THAN: @@ -220,18 +220,20 @@ static Truthness compareNumberValues(Number leftNumber, SqlExpressionEvaluator.C } } else { // if both values are not NaN, we can use the standard comparison logic - final Truthness truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForNumberComparison(leftNumber, rightNumber, comparisonOperatorType); + final Truthness truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForNumberComparison(expectedValueAsNumber, actualValueAsNumber, comparisonOperatorType); return truthnessOfComparison; } } - Truthness compareNullableValues(Object leftValue, SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType, Object rightValue) { - if (leftValue == null || rightValue == null) { + Truthness compareNullableValues(Object expectedValue, + ComparisonOperatorType comparisonOperatorType, + Object actualValue) { + if (expectedValue == null || actualValue == null) { switch (comparisonOperatorType) { case EQUALS_TO: - return (leftValue == null && rightValue == null) ? TRUE_C : C_FALSE; + return (expectedValue == null && actualValue == null) ? TRUE_C : C_FALSE; case NOT_EQUALS_TO: - return (leftValue == null && rightValue == null) ? C_FALSE : TRUE_C; + return (expectedValue == null && actualValue == null) ? C_FALSE : TRUE_C; case GREATER_THAN: case GREATER_THAN_EQUALS: case MINOR_THAN: @@ -241,8 +243,8 @@ Truthness compareNullableValues(Object leftValue, SqlExpressionEvaluator.Compari throw new IllegalArgumentException("Unsupported comparison operator type: " + comparisonOperatorType); } } else { - Truthness valTruthness = compareNonNullValues(rightValue, - comparisonOperatorType, leftValue + Truthness valTruthness = compareNonNullValues(actualValue, + comparisonOperatorType, expectedValue ); return buildSafeScaledTruthness(valTruthness); } @@ -265,7 +267,7 @@ Truthness computeHeuristicContainsElement(Object element, List list) { } else { Truthness res = buildOrAggregationTruthness(list.stream() .map(expectedValue -> compareNullableValues(expectedValue, - SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO, element + EQUALS_TO, element )) .toArray(Truthness[]::new)); return buildSafeScaledTruthness(res); @@ -321,11 +323,11 @@ Truthness evaluateMod(long divisor, long expectedRemainder, Object actualValue) Truthness evaluateBitsAllClearOperation(long bitmask, - Object value) { - if (!(value instanceof Number)) { + Object actualValue) { + if (!(actualValue instanceof Number)) { return C_FALSE; } - final Number number = (Number) value; + final Number number = (Number) actualValue; final OptionalLong integralValue = getIntegralLongValue(number); if (!integralValue.isPresent()) { @@ -339,12 +341,12 @@ Truthness evaluateBitsAllClearOperation(long bitmask, } Truthness evaluateBitsAllSetOperation(long bitmask, - Object value) { + Object actualValue) { - if (!(value instanceof Number)) { + if (!(actualValue instanceof Number)) { return C_FALSE; } - final Number number = (Number) value; + final Number number = (Number) actualValue; final OptionalLong integralValue = getIntegralLongValue(number); if (!integralValue.isPresent()) { return C_FALSE; @@ -359,12 +361,12 @@ Truthness evaluateBitsAllSetOperation(long bitmask, } Truthness evaluateBitsAnyClearOperation(long bitmask, - Object value) { + Object actualValue) { - if (!(value instanceof Number)) { + if (!(actualValue instanceof Number)) { return C_FALSE; } - final Number number = (Number) value; + final Number number = (Number) actualValue; final OptionalLong integralValue = getIntegralLongValue(number); if (!integralValue.isPresent()) { return C_FALSE; @@ -379,13 +381,13 @@ Truthness evaluateBitsAnyClearOperation(long bitmask, } Truthness evaluateBitsAnySetOperation(long bitmask, - Object value) { + Object actualValue) { - if (!(value instanceof Number)) { + if (!(actualValue instanceof Number)) { return C_FALSE; } - Number number = (Number) value; + Number number = (Number) actualValue; final OptionalLong integralValue = getIntegralLongValue(number); if (!integralValue.isPresent()) { return C_FALSE; @@ -439,40 +441,40 @@ static Truthness evaluateDistanceBetweenPoints(double minDistance, } Truthness evaluateEquality(Object actualValue, Object expectedValue) { - Truthness topLevelTruthness = this.compareNullableValues(actualValue, SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO, expectedValue); + Truthness topLevelTruthness = this.compareNullableValues(actualValue, EQUALS_TO, expectedValue); if (topLevelTruthness.isTrue()) { return topLevelTruthness; } else { return evaluateWithArrayUnwrapping(actualValue, value -> this.compareNullableValues( expectedValue, - SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO, value)); + EQUALS_TO, value)); } } /** - * Evaluates a nested structure or a single value using a provided heuristic function. + * Evaluates a nested structure or a single actualValue using a provided heuristic function. * For nested structures that are lists, it aggregates the results of applying the heuristic * function to each element in the list. For single values, it directly applies the heuristic. * - * @param value the value to evaluate, which can either be a single value or a list of values - * @param elementHeuristic a function to compute the heuristic for each element or the single value + * @param actualValue the actualValue to evaluate, which can either be a single actualValue or a list of values + * @param elementHeuristic a function to compute the heuristic for each element or the single actualValue * @return a Truthness object representing the heuristic of the evaluated input */ - Truthness evaluateWithArrayUnwrapping(Object value, + Truthness evaluateWithArrayUnwrapping(Object actualValue, Function elementHeuristic) { Objects.requireNonNull(elementHeuristic); - if (!(value instanceof List)) { - // value is not an array. Inspect only this value. - return elementHeuristic.apply(value); + if (!(actualValue instanceof List)) { + // actualValue is not an array. Inspect only this actualValue. + return elementHeuristic.apply(actualValue); } else { - // value is an array. Inspect all elements. - List values = (List) value; - if (values.isEmpty()) { + // actualValue is an array. Inspect all elements. + List actualValueList = (List) actualValue; + if (actualValueList.isEmpty()) { return C_FALSE; } - Truthness orAggregation = buildOrAggregationTruthness(values.stream() + Truthness orAggregation = buildOrAggregationTruthness(actualValueList.stream() .map(elementHeuristic) .toArray(Truthness[]::new)); return buildSafeScaledTruthness(orAggregation); From 01e1a8294cff746f6f9c8f1c06ac886c6e799162 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Thu, 17 Sep 2026 21:35:32 -0300 Subject: [PATCH 28/53] refactor: renaming to follow semantics --- .../MongoHeuristicsCalculatorHelper.java | 50 ++++++++++--------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java index db785170b3..693dbdc1bc 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java @@ -9,6 +9,7 @@ import org.evomaster.client.java.sql.heuristic.SqlExpressionEvaluator; import org.evomaster.client.java.sql.internal.TaintHandler; +import java.time.Instant; import java.util.Date; import java.util.List; import java.util.Objects; @@ -57,7 +58,7 @@ static Truthness buildSafeScaledTruthness(double maxOfTrue) { * Optionally, a {@link TaintHandler} can be used to handle tainting information * related to the regular expression processing. * - * @param actualValue the input string to be matched against the regular expression + * @param actualValue the input string to be matched against the regular expression * @param pattern the compiled {@link Pattern} representing the regular expression to match * @param taintHandler an optional implementation of {@link TaintHandler} to handle taint-related processing; * may be null if taint handling is not required @@ -136,50 +137,53 @@ Truthness evaluateListEquality(List actualList, List expectedList) { } Truthness compareNonNullValues(Object expectedValue, - SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType, + ComparisonOperatorType comparisonOperatorType, Object actualValue) { Objects.requireNonNull(expectedValue); Objects.requireNonNull(actualValue); final Truthness truthnessOfComparison; if (expectedValue instanceof Number && actualValue instanceof Number) { - final Number actualNumber = (Number) expectedValue; - final Number expectedNumber = (Number) actualValue; - truthnessOfComparison = compareNumberValues(actualNumber, comparisonOperatorType, expectedNumber); + final Number expectedValueAsNumber = (Number) expectedValue; + final Number actualNumberAsValue = (Number) actualValue; + truthnessOfComparison = compareNumberValues(expectedValueAsNumber, comparisonOperatorType, actualNumberAsValue); } else if (expectedValue instanceof String && actualValue instanceof String) { - String actualString = (String) expectedValue; - String expectedString = (String) actualValue; + String expectedValueAsString = (String) expectedValue; + String actualValueAsString = (String) actualValue; if (taintHandler != null && comparisonOperatorType == EQUALS_TO) { - taintHandler.handleTaintForStringEquals(actualString, expectedString, false); + taintHandler.handleTaintForStringEquals(expectedValueAsString, actualValueAsString, false); } - truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForStringComparison(actualString, expectedString, comparisonOperatorType); + truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForStringComparison(expectedValueAsString, actualValueAsString, comparisonOperatorType); } else if (expectedValue instanceof Boolean && actualValue instanceof Boolean) { - int actualIntValue = toIntValue((Boolean) expectedValue); - int expectedIntValue = toIntValue((Boolean) actualValue); + int expectedValueAsInt = toIntValue((Boolean) expectedValue); + int actualValueAsInt = toIntValue((Boolean) actualValue); truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForNumberComparison( - actualIntValue, expectedIntValue, comparisonOperatorType); + expectedValueAsInt, actualValueAsInt, comparisonOperatorType); } else if (expectedValue instanceof List && actualValue instanceof List) { - truthnessOfComparison = compareNonNullLists((List) expectedValue, comparisonOperatorType, (List) actualValue); + final List expectedValueAsList = (List) expectedValue; + final List actualValueAsList = (List) actualValue; + truthnessOfComparison = compareNonNullLists(expectedValueAsList, comparisonOperatorType, actualValueAsList); } else if (expectedValue instanceof Date && actualValue instanceof Date) { - truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForInstantComparison(convertToInstant(expectedValue), convertToInstant(actualValue), comparisonOperatorType); - + final Instant expectedValueAsInstant = convertToInstant(expectedValue); + final Instant actualValueAsInstant = convertToInstant(actualValue); + truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForInstantComparison(expectedValueAsInstant, actualValueAsInstant, comparisonOperatorType); } else if (BsonHelper.isBsonTimestamp(expectedValue) && BsonHelper.isBsonTimestamp(actualValue)) { - long actualTimestamp = BsonHelper.getBsonTimestampValue(expectedValue); - long expectedTimestamp = BsonHelper.getBsonTimestampValue(actualValue); - truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForNumberComparison(actualTimestamp, expectedTimestamp, comparisonOperatorType); + long expectedValueAsTimestampValue = BsonHelper.getBsonTimestampValue(expectedValue); + long actualValueAsTimestampValue = BsonHelper.getBsonTimestampValue(actualValue); + truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForNumberComparison(expectedValueAsTimestampValue, actualValueAsTimestampValue, comparisonOperatorType); } else if (BsonHelper.isObjectId(expectedValue) || BsonHelper.isObjectId(actualValue)) { - String actualString = expectedValue.toString(); - String expectedString = actualValue.toString(); + String expectedValueAsString = expectedValue.toString(); + String actualValueAsString = actualValue.toString(); if (taintHandler != null && comparisonOperatorType == EQUALS_TO) { - taintHandler.handleTaintForStringEquals(actualString, expectedString, false); + taintHandler.handleTaintForStringEquals(expectedValueAsString, actualValueAsString, false); } - truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForStringComparison(actualString, expectedString, comparisonOperatorType); + truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForStringComparison(expectedValueAsString, actualValueAsString, comparisonOperatorType); } else { // If both types are supported, but no actual comparison logic is defined, @@ -457,7 +461,7 @@ Truthness evaluateEquality(Object actualValue, Object expectedValue) { * For nested structures that are lists, it aggregates the results of applying the heuristic * function to each element in the list. For single values, it directly applies the heuristic. * - * @param actualValue the actualValue to evaluate, which can either be a single actualValue or a list of values + * @param actualValue the actualValue to evaluate, which can either be a single actualValue or a list of values * @param elementHeuristic a function to compute the heuristic for each element or the single actualValue * @return a Truthness object representing the heuristic of the evaluated input */ From 959df33c9212cd8363c75e49ed7c0a63aafd04f5 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Fri, 18 Sep 2026 14:08:54 -0300 Subject: [PATCH 29/53] add support for parsing org.bson.types.Binary and org.bson.BsonBinary --- .../controller/mongo/utils/BitmaskUtils.java | 28 +++++++++----- .../controller/mongo/utils/BsonHelper.java | 32 +++++++++++++++- .../mongo/MongoHeuristicsCalculatorTest.java | 1 - .../controller/mongo/QueryParserTest.java | 38 +++++++++++++++++++ .../mongo/utils/BsonHelperTest.java | 16 ++++++++ 5 files changed, 103 insertions(+), 12 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BitmaskUtils.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BitmaskUtils.java index 6141adba54..e431959a2f 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BitmaskUtils.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BitmaskUtils.java @@ -43,17 +43,25 @@ public static OptionalLong toBitMaskValue(Object value) { // value can be a byte array if (value instanceof byte[]) { byte[] bytes = (byte[]) value; - for (int i = Long.BYTES; i < bytes.length; i++) { - if (bytes[i] != 0) { - return OptionalLong.empty(); // significant bits above 63 - } - } - long mask = 0L; - for (int i = 0; i < Math.min(bytes.length, Long.BYTES); i++) { - mask |= (bytes[i] & 0xFFL) << (8 * i); // byte 0 holds positions 0-7 - } - return OptionalLong.of(mask); + return toBitMaskValue0(bytes); + } + if (BsonHelper.isBsonBinary(value)) { + byte[] bytes = BsonHelper.getBinaryData(value); + return toBitMaskValue0(bytes); } return OptionalLong.empty(); } + + private static OptionalLong toBitMaskValue0(byte[] bytes) { + for (int i = Long.BYTES; i < bytes.length; i++) { + if (bytes[i] != 0) { + return OptionalLong.empty(); + } + } + long mask = 0L; + for (int i = 0; i < Math.min(bytes.length, Long.BYTES); i++) { + mask |= (bytes[i] & 0xFFL) << (8 * i); // byte 0 holds positions 0-7 + } + return OptionalLong.of(mask); + } } diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BsonHelper.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BsonHelper.java index 6e7551baaa..d1f9994add 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BsonHelper.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BsonHelper.java @@ -18,12 +18,16 @@ public class BsonHelper { private static final String GET_PATTERN_METHOD = "getPattern"; private static final String GET_OPTIONS_METHOD = "getOptions"; + public static final String ORG_BSON_BSON_BINARY = "org.bson.BsonBinary"; private static final String ORG_BSON_BSON_TYPE = "org.bson.BsonType"; - private static final String ORG_BSON_DOCUMENT = "org.bson.Document"; private static final String BSON_REGEX_CLASS = "org.bson.BsonRegularExpression"; + private static final String ORG_BSON_DOCUMENT = "org.bson.Document"; + public static final String ORG_BSON_TYPES_BINARY = "org.bson.types.Binary"; public static final String NULL_TYPE = "null"; public static final String BSON_TYPE_NULL = "NULL"; + public static final String GET_DATA = "getData"; + public static Object newDocument(Object bsonDocument) { Objects.requireNonNull(bsonDocument); @@ -252,4 +256,30 @@ public static String bsonRegexGetOptions(Object value) { throw new RuntimeException(e); } } + + /** + * Checks if the given object is a BSON binary type. + * + * @param value the object to check; should be non-null to determine if it is a BSON binary type + * @return true if the object is non-null and of BSON binary type, false otherwise + */ + public static boolean isBsonBinary(Object value) { + if (value == null) { + return false; + } + String className = value.getClass().getName(); + return className.equals(ORG_BSON_BSON_BINARY) || className.equals(ORG_BSON_TYPES_BINARY); + } + + public static byte[] getBinaryData(Object value) { + Objects.requireNonNull(value, "The provided value cannot be null"); + if (!isBsonBinary(value)) { + throw new IllegalArgumentException("The provided value is not a BSON binary type but class: " + value.getClass().getName()); + } + try { + return (byte[]) value.getClass().getMethod(GET_DATA).invoke(value); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } } diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 9d96fbc679..13e3cb0e06 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -2159,7 +2159,6 @@ public void testRegexAgainstAnArrayWithoutStringElements() { } @Test - @Disabled("a bitmask held as org.bson.types.Binary is not parsed, so the calculator throws") public void testBitmaskGivenAsBinaryData() { /* mongo: matches, bit 1 of the mask is set and so is bit 1 of the value. diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java index 4cb0f600b9..d99a473e3b 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java @@ -1,18 +1,22 @@ package org.evomaster.client.java.controller.mongo; import com.mongodb.client.model.Filters; +import org.bson.BsonBinary; import org.bson.Document; import org.bson.BsonRegularExpression; import org.bson.codecs.DecoderContext; import org.bson.codecs.DocumentCodec; import org.bson.conversions.Bson; +import org.bson.types.Binary; import org.bson.types.Decimal128; import org.evomaster.client.java.controller.mongo.operations.*; +import org.evomaster.client.java.controller.mongo.utils.BitmaskUtils; import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.OptionalLong; import java.util.regex.Pattern; import static org.junit.jupiter.api.Assertions.*; @@ -1928,6 +1932,40 @@ void testParseInvalidNorWithInvalidCondition() { )); } + @Test + void testBinaryBitmask() { + Binary mask = new Binary(new byte[] { 0x30 }); // 0011 0000: bits 4 and 5 + + Document query = new Document( + "flags", + new Document("$bitsAllSet", mask) + ); + + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof BitsAllSetOperation); + BitsAllSetOperation bitsAllSet = (BitsAllSetOperation) operation; + OptionalLong expectedBitmask = BitmaskUtils.toBitMaskValue(mask); + assertTrue(expectedBitmask.isPresent()); + assertEquals(expectedBitmask.getAsLong(), bitsAllSet.getBitmask()); + } + + @Test + void testBsonBinaryBitmask() { + BsonBinary mask = new BsonBinary(new byte[] { 0x30 }); // 0011 0000: bits 4 and 5 + + Document query = new Document( + "flags", + new Document("$bitsAllSet", mask) + ); + + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof BitsAllSetOperation); + BitsAllSetOperation bitsAllSet = (BitsAllSetOperation) operation; + OptionalLong expectedBitmask = BitmaskUtils.toBitMaskValue(mask); + assertTrue(expectedBitmask.isPresent()); + assertEquals(expectedBitmask.getAsLong(), bitsAllSet.getBitmask()); + } + private ElemMatchOperation parseElemMatchCondition( Document condition, Class expectedConditionType) { diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/BsonHelperTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/BsonHelperTest.java index 9158063a9f..51744afcaf 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/BsonHelperTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/BsonHelperTest.java @@ -206,4 +206,20 @@ void testBsonRegexGettersRejectInvalidValues() { assertThrows(IllegalArgumentException.class, () -> BsonHelper.bsonRegexGetPattern("hospital")); assertThrows(IllegalArgumentException.class, () -> BsonHelper.bsonRegexGetOptions("hospital")); } + + @Test + void testIsBsonBinary() { + assertTrue(BsonHelper.isBsonBinary(new Binary((byte) 0x01, new byte[]{0x01, 0x02}))); + assertFalse(BsonHelper.isBsonBinary(new byte[]{0x01, 0x02})); + assertFalse(BsonHelper.isBsonBinary("not a Binary")); + assertFalse(BsonHelper.isBsonBinary(null)); + } + + @Test + void testGetBinaryData() { + byte[] data = new byte[]{0x01, 0x02, 0x03}; + Binary binary = new Binary((byte) 0x01, data); + + assertArrayEquals(data, BsonHelper.getBinaryData(binary)); + } } From 2549358c130e47d4aaa217c1e0cbda62ea75b9c6 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Fri, 18 Sep 2026 14:09:43 -0300 Subject: [PATCH 30/53] remove @Disabled for passing test --- .../java/controller/mongo/MongoHeuristicsCalculatorTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 13e3cb0e06..8245e2b420 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -2247,7 +2247,6 @@ public void testAllWithARepeatedElement() { } @Test - @Disabled("$all holding more than one null does not match a missing field") public void testAllWithRepeatedNullsAgainstAMissingField() { // mongo: matches, null matches a missing field however many times it is asked for Document doc = new Document().append("b", 1); From 7617de63a6cb049de870651a60533cedd4f14e47 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Fri, 18 Sep 2026 14:10:14 -0300 Subject: [PATCH 31/53] remove @Disabled for passing test --- .../java/controller/mongo/MongoHeuristicsCalculatorTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 8245e2b420..8e1b9631ec 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -2333,7 +2333,6 @@ public void testInequalityBetweenTwoSubDocuments() { } @Test - @Disabled("$eq does not match an array holding the given array as one of its elements") public void testEqualsMatchingAnArrayThatIsAnElement() { /* mongo: matches, the field holds [1,2] as one of its elements. The other half From b48cc8a7180ce834bf1fd0ea103cf37de3cd9d26 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Fri, 18 Sep 2026 14:38:54 -0300 Subject: [PATCH 32/53] add support of $eq and $ne comparisons among Binary values --- .../mongo/MongoHeuristicsCalculatorHelper.java | 18 ++++++++++++++++++ .../mongo/MongoHeuristicsCalculatorTest.java | 1 - 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java index 693dbdc1bc..0d122c552b 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java @@ -5,6 +5,7 @@ import org.evomaster.client.java.controller.mongo.utils.BsonHelper; import org.evomaster.client.java.controller.mongo.utils.MongoUtils; import org.evomaster.client.java.distance.heuristics.Truthness; +import org.evomaster.client.java.distance.heuristics.TruthnessUtils; import org.evomaster.client.java.instrumentation.coverage.methodreplacement.RegexDistanceUtils; import org.evomaster.client.java.sql.heuristic.SqlExpressionEvaluator; import org.evomaster.client.java.sql.internal.TaintHandler; @@ -185,6 +186,11 @@ Truthness compareNonNullValues(Object expectedValue, } truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForStringComparison(expectedValueAsString, actualValueAsString, comparisonOperatorType); + } else if (BsonHelper.isBsonBinary(expectedValue) && BsonHelper.isBsonBinary(actualValue)) { + byte[] expectedValueAsByteArray = BsonHelper.getBinaryData(expectedValue); + byte[] actualValueAsByteArray = BsonHelper.getBinaryData(actualValue); + + truthnessOfComparison = compareBinaryData(expectedValueAsByteArray, comparisonOperatorType, actualValueAsByteArray); } else { // If both types are supported, but no actual comparison logic is defined, // we considered them to be incompatible, therefore the comparison returns true @@ -194,6 +200,18 @@ Truthness compareNonNullValues(Object expectedValue, return truthnessOfComparison; } + private Truthness compareBinaryData(byte[] expectedValueAsByteArray, ComparisonOperatorType comparisonOperatorType, byte[] actualValueAsByteArray) { + TruthnessUtils.getEqualityTruthness(expectedValueAsByteArray, actualValueAsByteArray); + switch (comparisonOperatorType) { + case EQUALS_TO: + return getEqualityTruthness(expectedValueAsByteArray, actualValueAsByteArray); + case NOT_EQUALS_TO: + return getEqualityTruthness(expectedValueAsByteArray, actualValueAsByteArray).invert(); + default: + throw new IllegalArgumentException("Unsupported comparison operator type: " + comparisonOperatorType); + } + } + static Truthness compareNumberValues(Number expectedValueAsNumber, SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType, Number actualValueAsNumber) { Objects.requireNonNull(expectedValueAsNumber); Objects.requireNonNull(actualValueAsNumber); diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 8e1b9631ec..1d4e41eecf 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -2352,7 +2352,6 @@ public void testEqualsMatchingAnArrayThatIsAnElement() { } @Test - @Disabled("two binary values are never equal to one another, so the query never matches") public void testComparisonBetweenTwoBinaryValues() { /* mongo: the first three match and the last two do not. From f7b6c4380c6fdf8917dc3ce209f3620d702b8f47 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Fri, 18 Sep 2026 14:42:14 -0300 Subject: [PATCH 33/53] add TODO and exception message when $lte, $gte, $gt and $gte on byte[] values --- .../controller/mongo/MongoHeuristicsCalculatorHelper.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java index 0d122c552b..920d58b758 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java @@ -207,8 +207,14 @@ private Truthness compareBinaryData(byte[] expectedValueAsByteArray, ComparisonO return getEqualityTruthness(expectedValueAsByteArray, actualValueAsByteArray); case NOT_EQUALS_TO: return getEqualityTruthness(expectedValueAsByteArray, actualValueAsByteArray).invert(); + case GREATER_THAN: + case GREATER_THAN_EQUALS: + case MINOR_THAN: + case MINOR_THAN_EQUALS: + // TODO: Must implement comparison operator type for byte[] values. Currently only EQUALS_TO and NOT_EQUALS_TO are supported. + throw new IllegalArgumentException("Must implement comparison operator type: " + comparisonOperatorType); default: - throw new IllegalArgumentException("Unsupported comparison operator type: " + comparisonOperatorType); + throw new IllegalArgumentException("Unknown operator type: " + comparisonOperatorType); } } From bce0256606ed34f526d2f8632cc4cb35e4bade39 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Fri, 18 Sep 2026 15:24:37 -0300 Subject: [PATCH 34/53] $lte, $gte, $gt and $gte queries can accept null values / fix semantics for $gte and $gt when both values are null --- .../MongoHeuristicsCalculatorHelper.java | 4 +- .../selectors/GreaterThanEqualsSelector.java | 6 +- .../mongo/selectors/GreaterThanSelector.java | 7 +- .../selectors/LessThanEqualsSelector.java | 6 +- .../mongo/selectors/LessThanSelector.java | 6 +- .../mongo/MongoHeuristicsCalculatorTest.java | 45 +++++++-- .../controller/mongo/QueryParserTest.java | 96 +++++++++++-------- 7 files changed, 98 insertions(+), 72 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java index 920d58b758..03b4948040 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java @@ -258,14 +258,14 @@ Truthness compareNullableValues(Object expectedValue, Object actualValue) { if (expectedValue == null || actualValue == null) { switch (comparisonOperatorType) { + case MINOR_THAN_EQUALS: + case GREATER_THAN_EQUALS: case EQUALS_TO: return (expectedValue == null && actualValue == null) ? TRUE_C : C_FALSE; case NOT_EQUALS_TO: return (expectedValue == null && actualValue == null) ? C_FALSE : TRUE_C; case GREATER_THAN: - case GREATER_THAN_EQUALS: case MINOR_THAN: - case MINOR_THAN_EQUALS: return C_FALSE; default: throw new IllegalArgumentException("Unsupported comparison operator type: " + comparisonOperatorType); diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/GreaterThanEqualsSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/GreaterThanEqualsSelector.java index 0c2bca1f34..9649effadd 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/GreaterThanEqualsSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/GreaterThanEqualsSelector.java @@ -15,11 +15,7 @@ public class GreaterThanEqualsSelector extends SingleConditionQuerySelector { @Override protected QueryOperation parseValue(String fieldName, Object value) { Objects.requireNonNull(fieldName); - if (value==null) { - return null; - } else { - return new GreaterThanEqualsOperation<>(fieldName, value); - } + return new GreaterThanEqualsOperation<>(fieldName, value); } @Override diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/GreaterThanSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/GreaterThanSelector.java index f6ea4f2b07..45c9eb9775 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/GreaterThanSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/GreaterThanSelector.java @@ -15,12 +15,7 @@ public class GreaterThanSelector extends SingleConditionQuerySelector { @Override protected QueryOperation parseValue(String fieldName, Object value) { Objects.requireNonNull(fieldName); - - if (value == null) { - return null; - } else { - return new GreaterThanOperation<>(fieldName, value); - } + return new GreaterThanOperation<>(fieldName, value); } @Override diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/LessThanEqualsSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/LessThanEqualsSelector.java index e97608da29..ac50ee25cf 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/LessThanEqualsSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/LessThanEqualsSelector.java @@ -15,11 +15,7 @@ public class LessThanEqualsSelector extends SingleConditionQuerySelector { @Override protected QueryOperation parseValue(String fieldName, Object value) { Objects.requireNonNull(fieldName); - if (value==null) { - return null; - } else { - return new LessThanEqualsOperation<>(fieldName, value); - } + return new LessThanEqualsOperation<>(fieldName, value); } @Override diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/LessThanSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/LessThanSelector.java index ab774058be..c1748429bc 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/LessThanSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/LessThanSelector.java @@ -15,11 +15,7 @@ public class LessThanSelector extends SingleConditionQuerySelector { @Override protected QueryOperation parseValue(String fieldName, Object value) { Objects.requireNonNull(fieldName); - if (value==null) { - return null; - } else { - return new LessThanOperation<>(fieldName, value); - } + return new LessThanOperation<>(fieldName, value); } @Override diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 1d4e41eecf..6399e0b1c1 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -2407,7 +2407,6 @@ public void testAllHoldingAnElemMatch() { } @Test - @Disabled("an ordering operator given null is not parsed, so the calculator throws") public void testOrderingComparisonsGivenANullArgument() { /* mongo: runs all four and matches nothing, null orders against nothing. It is a @@ -2419,14 +2418,46 @@ public void testOrderingComparisonsGivenANullArgument() { Document doc = new Document().append("a", 1); MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); - for (String operator : Arrays.asList("$gt", "$gte", "$lt", "$lte")) { - Document query = new Document().append("a", new Document().append(operator, null)); - Truthness truthness = assertDoesNotThrow( - () -> calculator.computeHeuristicDocument(query, doc), operator); - assertTrue(truthness.isFalse(), operator); - } + Document queryGreaterThan = new Document().append("a", new Document().append("$gt", null)); + Truthness truthnessGreaterThan = calculator.computeHeuristicDocument(queryGreaterThan, doc); + assertTrue(truthnessGreaterThan.isFalse()); + + Document queryGreaterOrEqual = new Document().append("a", new Document().append("$gte", null)); + Truthness truthnessGreaterOrEqual = calculator.computeHeuristicDocument(queryGreaterOrEqual, doc); + assertTrue(truthnessGreaterOrEqual.isFalse()); + + Document queryLessThan = new Document().append("a", new Document().append("$lt", null)); + Truthness truthnessLessThan = calculator.computeHeuristicDocument(queryLessThan, doc); + assertTrue(truthnessLessThan.isFalse()); + + Document queryLessOrEqual = new Document().append("a", new Document().append("$lte", null)); + Truthness truthnessLessOrEqual = calculator.computeHeuristicDocument(queryLessOrEqual,doc); + assertTrue(truthnessLessOrEqual.isFalse()); } + @Test + public void testOrderingComparisonsGivenBothNullValues() { + Document doc = new Document().append("a", null); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + + Document queryGreaterThan = new Document().append("a", new Document().append("$gt", null)); + Truthness truthnessGreaterThan = calculator.computeHeuristicDocument(queryGreaterThan, doc); + assertTrue(truthnessGreaterThan.isFalse()); + + Document queryGreaterOrEqual = new Document().append("a", new Document().append("$gte", null)); + Truthness truthnessGreaterOrEqual = calculator.computeHeuristicDocument(queryGreaterOrEqual, doc); + assertTrue(truthnessGreaterOrEqual.isTrue()); + + Document queryLessThan = new Document().append("a", new Document().append("$lt", null)); + Truthness truthnessLessThan = calculator.computeHeuristicDocument(queryLessThan, doc); + assertTrue(truthnessLessThan.isFalse()); + + Document queryLessOrEqual = new Document().append("a", new Document().append("$lte", null)); + Truthness truthnessLessOrEqual = calculator.computeHeuristicDocument(queryLessOrEqual,doc); + assertTrue(truthnessLessOrEqual.isTrue()); + } + + @Test @Disabled("$exists given anything but a boolean is not parsed, so the calculator throws") public void testExistsGivenAValueThatIsNotABoolean() { diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java index d99a473e3b..5999ae28d5 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java @@ -1260,27 +1260,6 @@ void testParseInvalidEmptyOperatorDocument() { assertNull(operation); } - @Test - void testParseInvalidComparisonOperatorWithMissingValue() { - Document query = new Document( - "age", - new Document("$lt", null) - ); - - QueryOperation operation = parser.parse(query); - - assertNull(operation); - } - - @Test - void testParseInvalidLessThanNull() { - Document query = new Document( - "age", - new Document("$lt", null) - ); - QueryOperation operation = parser.parse(query); - assertNull(operation); - } @Test void testParseInvalidLessEqualsThanNull() { @@ -1292,27 +1271,6 @@ void testParseInvalidLessEqualsThanNull() { assertNull(operation); } - @Test - void testParseGreaterThenEqualsThanNull() { - Document query = new Document( - "age", - new Document("$gte", null) - ); - QueryOperation operation = parser.parse(query); - assertNull(operation); - } - - @Test - void testParseGreaterThenThanNull() { - Document query = new Document( - "age", - new Document("$gt", null) - ); - QueryOperation operation = parser.parse(query); - assertNull(operation); - } - - @Test void testParseInvalidMultipleOperatorsIncludingUnknownOperator() { Document query = new Document( @@ -1966,6 +1924,60 @@ void testBsonBinaryBitmask() { assertEquals(expectedBitmask.getAsLong(), bitsAllSet.getBitmask()); } + @Test + void testParseGreaterThanOrEqualsNull() { + Document query = new Document( + "age", + new Document("$gte", null) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof GreaterThanEqualsOperation); + GreaterThanEqualsOperation gte = (GreaterThanEqualsOperation) operation; + assertEquals("age", gte.getFieldName()); + assertEquals(null, gte.getValue()); + } + + @Test + void testParseGreaterThanNull() { + Document query = new Document( + "age", + new Document("$gt", null) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof GreaterThanOperation); + GreaterThanOperation gt = (GreaterThanOperation) operation; + assertEquals("age", gt.getFieldName()); + assertEquals(null, gt.getValue()); + } + + @Test + void testParseLesserThanNull() { + Document query = new Document( + "age", + new Document("$lt", null) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof LessThanOperation); + LessThanOperation lt = (LessThanOperation) operation; + assertEquals("age", lt.getFieldName()); + assertEquals(null, lt.getValue()); + } + + @Test + void testParseLesserThanOrEqualNull() { + Document query = new Document( + "age", + new Document("$lte", null) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof LessThanEqualsOperation); + LessThanEqualsOperation lte = (LessThanEqualsOperation) operation; + assertEquals("age", lte.getFieldName()); + assertEquals(null, lte.getValue()); + } + + + private ElemMatchOperation parseElemMatchCondition( Document condition, Class expectedConditionType) { From bdc14482f8a328b335071dcd18317c098f6be861 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Fri, 18 Sep 2026 17:13:41 -0300 Subject: [PATCH 35/53] comparison operations for BsonRegularExpression data type --- .../MongoHeuristicsCalculatorHelper.java | 5 +++++ .../mongo/MongoHeuristicsCalculatorTest.java | 20 +++++++++++++++++++ .../controller/mongo/QueryParserTest.java | 8 ++++++++ 3 files changed, 33 insertions(+) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java index 03b4948040..69eaf267d1 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java @@ -178,6 +178,11 @@ Truthness compareNonNullValues(Object expectedValue, long actualValueAsTimestampValue = BsonHelper.getBsonTimestampValue(actualValue); truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForNumberComparison(expectedValueAsTimestampValue, actualValueAsTimestampValue, comparisonOperatorType); + }else if (BsonHelper.isBsonRegularExpression(expectedValue) && BsonHelper.isBsonRegularExpression(actualValue)) { + String expectedValuePatternAsString = BsonHelper.bsonRegexGetPattern(expectedValue); + String actualValuePatternAsString = BsonHelper.bsonRegexGetPattern(actualValue); + truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForStringComparison(expectedValuePatternAsString, actualValuePatternAsString, comparisonOperatorType); + } else if (BsonHelper.isObjectId(expectedValue) || BsonHelper.isObjectId(actualValue)) { String expectedValueAsString = expectedValue.toString(); String actualValueAsString = actualValue.toString(); diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 6399e0b1c1..efff49c5c7 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -2406,6 +2406,26 @@ public void testAllHoldingAnElemMatch() { assertTrue(calculator.computeHeuristicDocument(two, doc).isTrue()); } + @Test + void testEqBsonRegularExpression() { + Document doc = new Document().append("name", "x"); + Document query = new Document("name", + new Document("$eq", new BsonRegularExpression("x"))); + + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + assertTrue(calculator.computeHeuristicDocument(query, doc).isFalse()); + } + + @Test + void testEqBetweenBsonRegularExpression() { + Document doc = new Document().append("name", new BsonRegularExpression("x")); + Document query = new Document("name", + new Document("$eq", new BsonRegularExpression("x"))); + + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + assertTrue(calculator.computeHeuristicDocument(query, doc).isTrue()); + } + @Test public void testOrderingComparisonsGivenANullArgument() { /* diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java index 5999ae28d5..71fbb24d6c 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java @@ -435,6 +435,14 @@ void testParseRegexBsonRegularExpression() { assertFalse(regex.getOptions().isMultiline()); } + @Test + void testParseEqBsonRegularExpression() { + Document query = new Document("name", + new Document("$eq", new BsonRegularExpression("x"))); + + assertNotNull(parser.parse(query)); + } + @Test void testParseFiltersRegex() { Document query = convertToDocument(Filters.regex("name", "^hospital.*", "i")); From 848f00d8ac2bca9dabaff8bbfea3fa44f4abbfbf Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Fri, 18 Sep 2026 17:41:33 -0300 Subject: [PATCH 36/53] fix comparison to {"a": {"$not": /x/}} --- .../java/controller/mongo/selectors/NotSelector.java | 9 +++++++++ .../controller/mongo/selectors/RegexSelector.java | 8 ++++++-- .../mongo/MongoHeuristicsCalculatorTest.java | 1 - .../java/controller/mongo/QueryParserTest.java | 12 ++++++++++++ 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/NotSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/NotSelector.java index f8d010ede1..df035d5e39 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/NotSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/NotSelector.java @@ -45,6 +45,15 @@ protected QueryOperation parseValue(String fieldName, Object value) { } else { return new NotOperation(fieldName, condition); } + } else if (isBsonRegularExpression(value)) { + String pattern = bsonRegexGetPattern(value); + String options = bsonRegexGetOptions(value); + RegexOperation regexOperation = RegexSelector.buildRegexOperation(fieldName, pattern, options); + if (regexOperation == null) { + return null; + } else { + return new NotOperation(fieldName, regexOperation); + } } else { return null; } diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/RegexSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/RegexSelector.java index 29f70c5419..284cb8aad7 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/RegexSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/RegexSelector.java @@ -102,6 +102,10 @@ public QueryOperation getOperation(Object query) { return null; } + return buildRegexOperation(fieldName, pattern, options); + } + + public static RegexOperation buildRegexOperation(String fieldName, String pattern, String options) { RegexOptions parsedOptions = parseOptions(options); if (parsedOptions == null) { return null; @@ -117,7 +121,7 @@ public QueryOperation getOperation(Object query) { return new RegexOperation(fieldName, compile, parsedOptions); } - private RegexOptions parseOptions(String options) { + private static RegexOptions parseOptions(String options) { for (char option : options.toCharArray()) { if (!REGEX_OPTIONS.contains(option)) { @@ -132,7 +136,7 @@ private RegexOptions parseOptions(String options) { options.indexOf(MONGO_REGEX_OPTION_UNICODE) >= 0); } - private int javaRegexFlagsFromOptions(RegexOptions options) { + private static int javaRegexFlagsFromOptions(RegexOptions options) { int flags = 0; if (options.isCaseInsensitive()) { flags |= Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE; diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index efff49c5c7..bb5668eb55 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -2188,7 +2188,6 @@ public void testBitmaskGivenAsAByteArray() { } @Test - @Disabled("$not holding a bare regex is not parsed, so the calculator throws a NullPointerException") public void testNotWithABareRegex() { /* mongo: matches, {"a": {"$not": /x/}} is a documented form of the operator. diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java index 71fbb24d6c..4492ba1e1d 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java @@ -435,6 +435,18 @@ void testParseRegexBsonRegularExpression() { assertFalse(regex.getOptions().isMultiline()); } + @Test + void testParseNotRegexBsonRegularExpression() { + Document query = new Document("name", + new Document("$not", new BsonRegularExpression("x"))); + + NotOperation notOperation = assertInstanceOf(NotOperation.class, parser.parse(query)); + assertEquals("name", notOperation.getFieldName()); + assertTrue(notOperation.getCondition() instanceof RegexOperation); + RegexOperation regex = (RegexOperation) notOperation.getCondition(); + assertEquals("x", regex.getPattern().pattern()); + } + @Test void testParseEqBsonRegularExpression() { Document query = new Document("name", From 2ca76a5108dc9647a12f5b71e29c606fc41910ff Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Fri, 18 Sep 2026 20:07:13 -0300 Subject: [PATCH 37/53] fix semantics for $eq, $lte, $gt, $gte, $ne of documents --- .../mongo/MongoHeuristicsCalculator.java | 32 ++-- .../MongoHeuristicsCalculatorHelper.java | 169 ++++++++++++------ .../controller/mongo/utils/BsonHelper.java | 1 + .../mongo/MongoHeuristicsCalculatorTest.java | 70 +++++++- 4 files changed, 193 insertions(+), 79 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java index 47ae8f2f9f..6f590c25c2 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java @@ -239,9 +239,8 @@ private Truthness evaluate(NearOperation operation, Object actualValue) { final double maxDistance = operation.hasMaxDistance() ? operation.getMaxDistance() : Double.MAX_VALUE; return helper.evaluateDistanceBetweenPoints( - minDistance, maxDistance, + actualValue, minDistance, maxDistance, longitude, latitude, - actualValue, model); } @@ -301,9 +300,7 @@ private Truthness evaluate(NotEqualsOperation operation, Object actualValue) return helper.computeHeuristicContainsElement(expectedValue, (List) actualValue).invert(); } else { return helper.compareNullableValues( - expectedValue, - NOT_EQUALS_TO, - actualValue + actualValue, NOT_EQUALS_TO, expectedValue ); } } @@ -315,9 +312,7 @@ private Truthness evaluate(GreaterThanOperation operation, Object actualValue Object expectedValue = operation.getValue(); return helper.evaluateWithArrayUnwrapping(actualValue, value -> helper.compareNullableValues( - expectedValue, - GREATER_THAN, - value + value, GREATER_THAN, expectedValue )); } @@ -327,7 +322,7 @@ private Truthness evaluate(GreaterThanEqualsOperation operation, Object actua final Object expectedValue = operation.getValue(); return helper.evaluateWithArrayUnwrapping(actualValue, - value -> helper.compareNullableValues(expectedValue, GREATER_THAN_EQUALS, value)); + value -> helper.compareNullableValues(value, GREATER_THAN_EQUALS, expectedValue)); } private Truthness evaluate(LessThanOperation operation, Object actualValue) { @@ -335,7 +330,7 @@ private Truthness evaluate(LessThanOperation operation, Object actualValue) { final Object expectedValue = operation.getValue(); return helper.evaluateWithArrayUnwrapping(actualValue, - value -> helper.compareNullableValues(expectedValue, MINOR_THAN, value)); + value -> helper.compareNullableValues(value, MINOR_THAN, expectedValue)); } private Truthness evaluate(LessThanEqualsOperation operation, Object actualValue) { @@ -343,7 +338,7 @@ private Truthness evaluate(LessThanEqualsOperation operation, Object actualVa final Object expectedValue = operation.getValue(); return helper.evaluateWithArrayUnwrapping(actualValue, - value -> helper.compareNullableValues(expectedValue, MINOR_THAN_EQUALS, value + value -> helper.compareNullableValues(value, MINOR_THAN_EQUALS, expectedValue )); } @@ -473,7 +468,7 @@ private Truthness computeHeuristic(ExistsOperation operation, Object input) { } else { Truthness orTruthness = buildOrAggregationTruthness(actualFieldNames.stream() .map(actualFieldName -> - helper.compareNonNullValues(expectedFieldName, EQUALS_TO, actualFieldName)) + helper.compareNonNullValues(actualFieldName, EQUALS_TO, expectedFieldName)) .toArray(Truthness[]::new)); res = buildSafeScaledTruthness(orTruthness); } @@ -501,31 +496,31 @@ private Truthness evaluate(ModOperation operation, Object actualValue) { long divisor = operation.getDivisor().longValue(); long expectedRemainder = operation.getRemainder().longValue(); return helper.evaluateWithArrayUnwrapping(actualValue, - value -> helper.evaluateMod(divisor, expectedRemainder, value)); + value -> helper.evaluateMod(value, divisor, expectedRemainder)); } private Truthness evaluate(BitsAllClearOperation operation, Object actualValue) { Objects.requireNonNull(operation); return helper.evaluateWithArrayUnwrapping(actualValue, - value -> helper.evaluateBitsAllClearOperation(operation.getBitmask(), value)); + value -> helper.evaluateBitsAllClearOperation(value, operation.getBitmask())); } private Truthness evaluate(BitsAnyClearOperation operation, Object actualValue) { Objects.requireNonNull(operation); return helper.evaluateWithArrayUnwrapping(actualValue, - value -> helper.evaluateBitsAnyClearOperation(operation.getBitmask(), value)); + value -> helper.evaluateBitsAnyClearOperation(value, operation.getBitmask())); } private Truthness evaluate(BitsAllSetOperation operation, Object actualValue) { Objects.requireNonNull(operation); return helper.evaluateWithArrayUnwrapping(actualValue, - value -> helper.evaluateBitsAllSetOperation(operation.getBitmask(), value)); + value -> helper.evaluateBitsAllSetOperation(value, operation.getBitmask())); } private Truthness evaluate(BitsAnySetOperation operation, Object actualValue) { Objects.requireNonNull(operation); return helper.evaluateWithArrayUnwrapping(actualValue, - value -> helper.evaluateBitsAnySetOperation(operation.getBitmask(), value)); + value -> helper.evaluateBitsAnySetOperation(value, operation.getBitmask())); } @@ -579,9 +574,8 @@ private Truthness evaluate(NearSphereOperation operation, Object actualValue) { final double minDistance = operation.hasMinDistance() ? operation.getMinDistance() : 0.0; return helper.evaluateDistanceBetweenPoints( - minDistance, maxDistance, + actualValue, minDistance, maxDistance, longitude, latitude, - actualValue, SPHERICAL); } diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java index 69eaf267d1..b8c3d678fa 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java @@ -11,21 +11,19 @@ import org.evomaster.client.java.sql.internal.TaintHandler; import java.time.Instant; -import java.util.Date; -import java.util.List; -import java.util.Objects; -import java.util.OptionalLong; +import java.util.*; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; +import static org.evomaster.client.java.controller.mongo.utils.BsonHelper.documentKeys; import static org.evomaster.client.java.controller.mongo.utils.BsonHelper.isBsonDocument; import static org.evomaster.client.java.controller.mongo.utils.MongoUtils.GeoSpatialModel.SPHERICAL; import static org.evomaster.client.java.controller.mongo.utils.MongoUtils.getDistanceBetweenPoints; import static org.evomaster.client.java.controller.mongo.utils.MongoUtils.getIntegralLongValue; import static org.evomaster.client.java.distance.heuristics.TruthnessUtils.*; import static org.evomaster.client.java.sql.heuristic.ConversionHelper.convertToInstant; -import static org.evomaster.client.java.sql.heuristic.SqlExpressionEvaluator.ComparisonOperatorType; +import static org.evomaster.client.java.sql.heuristic.SqlExpressionEvaluator.*; import static org.evomaster.client.java.sql.heuristic.SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO; import static org.evomaster.client.java.sql.heuristic.SqlExpressionEvaluator.ComparisonOperatorType.NOT_EQUALS_TO; @@ -103,18 +101,19 @@ static Truthness buildSafeScaledTruthness(Truthness truthness) { return buildSafeScaledTruthness(truthness.getOfTrue()); } - Truthness compareNonNullLists(List expectedValueAsList, ComparisonOperatorType comparisonOperatorType, List actualValueAsList) { + Truthness compareNonNullLists(List actualValueAsList, ComparisonOperatorType op, List expectedValueAsList) { + Objects.requireNonNull(expectedValueAsList); Objects.requireNonNull(actualValueAsList); final Truthness truthness = evaluateListEquality(expectedValueAsList, actualValueAsList); - switch (comparisonOperatorType) { + switch (op) { case EQUALS_TO: return truthness; case NOT_EQUALS_TO: return truthness.invert(); default: - throw new IllegalArgumentException("Unsupported binary operator: " + comparisonOperatorType); + throw new IllegalArgumentException("Unsupported binary operator: " + op); } } @@ -137,9 +136,48 @@ Truthness evaluateListEquality(List actualList, List expectedList) { return truthness; } - Truthness compareNonNullValues(Object expectedValue, - ComparisonOperatorType comparisonOperatorType, - Object actualValue) { + List compareDocuments(Object actualValue, ComparisonOperatorType op, Object expectedValue) { + + Objects.requireNonNull(actualValue); + Objects.requireNonNull(op); + Objects.requireNonNull(expectedValue); + if (!isBsonDocument(expectedValue) || !isBsonDocument(actualValue)) { + throw new IllegalArgumentException("Both expected and actual values must be BSON documents."); + } + + List truthnesses = new ArrayList<>(); + Set expectedFieldNames = documentKeys(expectedValue); + Set actualFieldNames = documentKeys(actualValue); + /** + * org.bson.Document preserves insertion order. This means that the order of field names + * in the expected and actual documents may differ even if they contain the same fields. + */ + Iterator expectedFieldNamesIterator = expectedFieldNames.iterator(); + Iterator actualFieldNamesIterator = actualFieldNames.iterator(); + while (expectedFieldNamesIterator.hasNext() && actualFieldNamesIterator.hasNext()) { + String expectedFieldName = expectedFieldNamesIterator.next(); + String actualFieldName = actualFieldNamesIterator.next(); + Truthness fieldNameComparisonTruthness = compareNonNullValues(actualFieldName, op, expectedFieldName); + truthnesses.add(fieldNameComparisonTruthness); + if (!expectedFieldName.equals(actualFieldName)) { + return truthnesses; + } + Object expectedFieldValue = BsonHelper.getValue(expectedValue, expectedFieldName); + Object actualFieldValue = BsonHelper.getValue(actualValue, expectedFieldName); + Truthness fieldValueComparisonTruthness = compareNullableValues(actualFieldValue, op, expectedFieldValue); + truthnesses.add(fieldValueComparisonTruthness); + if (!Objects.equals(expectedFieldValue, actualFieldValue)) { + return truthnesses; + } + } + Truthness fieldTruthness = compareNonNullValues(actualFieldNamesIterator.hasNext(), op, expectedFieldNamesIterator.hasNext()); + truthnesses.add(fieldTruthness); + return truthnesses; + } + + + Truthness compareNonNullValues(Object actualValue, ComparisonOperatorType op, Object expectedValue) { + Objects.requireNonNull(expectedValue); Objects.requireNonNull(actualValue); @@ -147,93 +185,115 @@ Truthness compareNonNullValues(Object expectedValue, if (expectedValue instanceof Number && actualValue instanceof Number) { final Number expectedValueAsNumber = (Number) expectedValue; final Number actualNumberAsValue = (Number) actualValue; - truthnessOfComparison = compareNumberValues(expectedValueAsNumber, comparisonOperatorType, actualNumberAsValue); + truthnessOfComparison = compareNumberValues(actualNumberAsValue, op, expectedValueAsNumber); } else if (expectedValue instanceof String && actualValue instanceof String) { String expectedValueAsString = (String) expectedValue; String actualValueAsString = (String) actualValue; - if (taintHandler != null && comparisonOperatorType == EQUALS_TO) { + if (taintHandler != null && op == EQUALS_TO) { taintHandler.handleTaintForStringEquals(expectedValueAsString, actualValueAsString, false); } - truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForStringComparison(expectedValueAsString, actualValueAsString, comparisonOperatorType); + truthnessOfComparison = calculateTruthnessForStringComparison( + actualValueAsString, expectedValueAsString, op); } else if (expectedValue instanceof Boolean && actualValue instanceof Boolean) { int expectedValueAsInt = toIntValue((Boolean) expectedValue); int actualValueAsInt = toIntValue((Boolean) actualValue); - truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForNumberComparison( - expectedValueAsInt, actualValueAsInt, comparisonOperatorType); + truthnessOfComparison = calculateTruthnessForNumberComparison( + actualValueAsInt, expectedValueAsInt, op); } else if (expectedValue instanceof List && actualValue instanceof List) { final List expectedValueAsList = (List) expectedValue; final List actualValueAsList = (List) actualValue; - truthnessOfComparison = compareNonNullLists(expectedValueAsList, comparisonOperatorType, actualValueAsList); + truthnessOfComparison = compareNonNullLists(actualValueAsList, op, expectedValueAsList); } else if (expectedValue instanceof Date && actualValue instanceof Date) { final Instant expectedValueAsInstant = convertToInstant(expectedValue); final Instant actualValueAsInstant = convertToInstant(actualValue); - truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForInstantComparison(expectedValueAsInstant, actualValueAsInstant, comparisonOperatorType); + truthnessOfComparison = calculateTruthnessForInstantComparison( + actualValueAsInstant, expectedValueAsInstant, op); } else if (BsonHelper.isBsonTimestamp(expectedValue) && BsonHelper.isBsonTimestamp(actualValue)) { long expectedValueAsTimestampValue = BsonHelper.getBsonTimestampValue(expectedValue); long actualValueAsTimestampValue = BsonHelper.getBsonTimestampValue(actualValue); - truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForNumberComparison(expectedValueAsTimestampValue, actualValueAsTimestampValue, comparisonOperatorType); + truthnessOfComparison = calculateTruthnessForNumberComparison(actualValueAsTimestampValue, expectedValueAsTimestampValue, op); - }else if (BsonHelper.isBsonRegularExpression(expectedValue) && BsonHelper.isBsonRegularExpression(actualValue)) { + } else if (BsonHelper.isBsonRegularExpression(expectedValue) && BsonHelper.isBsonRegularExpression(actualValue)) { String expectedValuePatternAsString = BsonHelper.bsonRegexGetPattern(expectedValue); String actualValuePatternAsString = BsonHelper.bsonRegexGetPattern(actualValue); - truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForStringComparison(expectedValuePatternAsString, actualValuePatternAsString, comparisonOperatorType); + truthnessOfComparison = calculateTruthnessForStringComparison(actualValuePatternAsString, expectedValuePatternAsString, op); } else if (BsonHelper.isObjectId(expectedValue) || BsonHelper.isObjectId(actualValue)) { String expectedValueAsString = expectedValue.toString(); String actualValueAsString = actualValue.toString(); - if (taintHandler != null && comparisonOperatorType == EQUALS_TO) { + if (taintHandler != null && op == EQUALS_TO) { taintHandler.handleTaintForStringEquals(expectedValueAsString, actualValueAsString, false); } - truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForStringComparison(expectedValueAsString, actualValueAsString, comparisonOperatorType); + truthnessOfComparison = calculateTruthnessForStringComparison(actualValueAsString, expectedValueAsString, op); } else if (BsonHelper.isBsonBinary(expectedValue) && BsonHelper.isBsonBinary(actualValue)) { byte[] expectedValueAsByteArray = BsonHelper.getBinaryData(expectedValue); byte[] actualValueAsByteArray = BsonHelper.getBinaryData(actualValue); - truthnessOfComparison = compareBinaryData(expectedValueAsByteArray, comparisonOperatorType, actualValueAsByteArray); + truthnessOfComparison = compareBinaryData(actualValueAsByteArray, op, expectedValueAsByteArray); + } else if (BsonHelper.isBsonDocument(expectedValue) && BsonHelper.isBsonDocument(actualValue)) { + truthnessOfComparison = compareDocumentValues(actualValue, op, expectedValue); } else { // If both types are supported, but no actual comparison logic is defined, // we considered them to be incompatible, therefore the comparison returns true // only if the comparison operator is NOT_EQUALS_TO. Otherwise returns false. - truthnessOfComparison = comparisonOperatorType == NOT_EQUALS_TO ? TRUE_C : C_FALSE; + truthnessOfComparison = op == NOT_EQUALS_TO ? TRUE_C : C_FALSE; } return truthnessOfComparison; } - private Truthness compareBinaryData(byte[] expectedValueAsByteArray, ComparisonOperatorType comparisonOperatorType, byte[] actualValueAsByteArray) { - TruthnessUtils.getEqualityTruthness(expectedValueAsByteArray, actualValueAsByteArray); - switch (comparisonOperatorType) { + private Truthness compareDocumentValues(Object actualValue, + ComparisonOperatorType op, + Object expectedValue) { + + Objects.requireNonNull(expectedValue); + Objects.requireNonNull(op); + Objects.requireNonNull(actualValue); + if (!isBsonDocument(expectedValue) || !isBsonDocument(actualValue)) { + throw new IllegalArgumentException("Both expected and actual values must be BSON documents."); + } + + List truthnesses = compareDocuments(actualValue, op, expectedValue); + return buildAndAggregationTruthness(truthnesses.toArray(new Truthness[0])); + } + + private Truthness compareBinaryData(byte[] expectedValueAsByteArray, + ComparisonOperatorType op, + byte[] actualValueAsByteArray) { + + final Truthness equalityTruthness = getEqualityTruthness(actualValueAsByteArray, expectedValueAsByteArray); + switch (op) { case EQUALS_TO: - return getEqualityTruthness(expectedValueAsByteArray, actualValueAsByteArray); + return equalityTruthness; case NOT_EQUALS_TO: - return getEqualityTruthness(expectedValueAsByteArray, actualValueAsByteArray).invert(); + return equalityTruthness.invert(); case GREATER_THAN: case GREATER_THAN_EQUALS: case MINOR_THAN: case MINOR_THAN_EQUALS: // TODO: Must implement comparison operator type for byte[] values. Currently only EQUALS_TO and NOT_EQUALS_TO are supported. - throw new IllegalArgumentException("Must implement comparison operator type: " + comparisonOperatorType); + throw new IllegalArgumentException("Must implement comparison operator type: " + op); default: - throw new IllegalArgumentException("Unknown operator type: " + comparisonOperatorType); + throw new IllegalArgumentException("Unknown operator type: " + op); } } - static Truthness compareNumberValues(Number expectedValueAsNumber, SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType, Number actualValueAsNumber) { + static Truthness compareNumberValues(Number actualValueAsNumber, ComparisonOperatorType op, Number expectedValueAsNumber) { Objects.requireNonNull(expectedValueAsNumber); + Objects.requireNonNull(op); Objects.requireNonNull(actualValueAsNumber); - Objects.requireNonNull(comparisonOperatorType); double expectedValueAsDouble = expectedValueAsNumber.doubleValue(); double actualValueAsDouble = actualValueAsNumber.doubleValue(); if (Double.isNaN(expectedValueAsDouble) || Double.isNaN(actualValueAsDouble)) { // handle case when NaN is involved in the comparison - switch (comparisonOperatorType) { + switch (op) { case EQUALS_TO: { return (Double.isNaN(expectedValueAsDouble) && Double.isNaN(actualValueAsDouble)) ? TRUE_C : C_FALSE; @@ -249,20 +309,18 @@ static Truthness compareNumberValues(Number expectedValueAsNumber, SqlExpression return C_FALSE; } default: - throw new IllegalArgumentException("Unsupported comparison operator type: " + comparisonOperatorType); + throw new IllegalArgumentException("Unsupported comparison operator type: " + op); } } else { // if both values are not NaN, we can use the standard comparison logic - final Truthness truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForNumberComparison(expectedValueAsNumber, actualValueAsNumber, comparisonOperatorType); + final Truthness truthnessOfComparison = calculateTruthnessForNumberComparison(actualValueAsNumber, expectedValueAsNumber, op); return truthnessOfComparison; } } - Truthness compareNullableValues(Object expectedValue, - ComparisonOperatorType comparisonOperatorType, - Object actualValue) { + Truthness compareNullableValues(Object actualValue, ComparisonOperatorType op, Object expectedValue) { if (expectedValue == null || actualValue == null) { - switch (comparisonOperatorType) { + switch (op) { case MINOR_THAN_EQUALS: case GREATER_THAN_EQUALS: case EQUALS_TO: @@ -273,11 +331,10 @@ Truthness compareNullableValues(Object expectedValue, case MINOR_THAN: return C_FALSE; default: - throw new IllegalArgumentException("Unsupported comparison operator type: " + comparisonOperatorType); + throw new IllegalArgumentException("Unsupported comparison operator type: " + op); } } else { - Truthness valTruthness = compareNonNullValues(actualValue, - comparisonOperatorType, expectedValue + Truthness valTruthness = compareNonNullValues(actualValue, op, expectedValue ); return buildSafeScaledTruthness(valTruthness); } @@ -299,8 +356,7 @@ Truthness computeHeuristicContainsElement(Object element, List list) { return C_FALSE; } else { Truthness res = buildOrAggregationTruthness(list.stream() - .map(expectedValue -> compareNullableValues(expectedValue, - EQUALS_TO, element + .map(expectedValue -> compareNullableValues(element, EQUALS_TO, expectedValue )) .toArray(Truthness[]::new)); return buildSafeScaledTruthness(res); @@ -344,7 +400,7 @@ Truthness computeHeuristicInOperation(Object actualValue, List expectedValueL return res; } - Truthness evaluateMod(long divisor, long expectedRemainder, Object actualValue) { + Truthness evaluateMod(Object actualValue, long divisor, long expectedRemainder) { if (!(actualValue instanceof Number)) { return C_FALSE; } @@ -355,8 +411,7 @@ Truthness evaluateMod(long divisor, long expectedRemainder, Object actualValue) } - Truthness evaluateBitsAllClearOperation(long bitmask, - Object actualValue) { + Truthness evaluateBitsAllClearOperation(Object actualValue, long bitmask) { if (!(actualValue instanceof Number)) { return C_FALSE; } @@ -373,8 +428,7 @@ Truthness evaluateBitsAllClearOperation(long bitmask, return buildSafeScaledTruthness(equalityTruthness); } - Truthness evaluateBitsAllSetOperation(long bitmask, - Object actualValue) { + Truthness evaluateBitsAllSetOperation(Object actualValue, long bitmask) { if (!(actualValue instanceof Number)) { return C_FALSE; @@ -393,8 +447,7 @@ Truthness evaluateBitsAllSetOperation(long bitmask, return buildSafeScaledTruthness(equalityTruthness); } - Truthness evaluateBitsAnyClearOperation(long bitmask, - Object actualValue) { + Truthness evaluateBitsAnyClearOperation(Object actualValue, long bitmask) { if (!(actualValue instanceof Number)) { return C_FALSE; @@ -413,8 +466,7 @@ Truthness evaluateBitsAnyClearOperation(long bitmask, return buildSafeScaledTruthness(lessThanTruthness); } - Truthness evaluateBitsAnySetOperation(long bitmask, - Object actualValue) { + Truthness evaluateBitsAnySetOperation(Object actualValue, long bitmask) { if (!(actualValue instanceof Number)) { return C_FALSE; @@ -432,11 +484,10 @@ Truthness evaluateBitsAnySetOperation(long bitmask, return buildSafeScaledTruthness(lessThanTruthness); } - static Truthness evaluateDistanceBetweenPoints(double minDistance, + static Truthness evaluateDistanceBetweenPoints(Object actualValue, double minDistance, double maxDistance, double longitude, double latitude, - Object actualValue, MongoUtils.GeoSpatialModel geoSpatialModel) { double x1 = geoSpatialModel == SPHERICAL ? Math.toRadians(longitude) : longitude; @@ -480,8 +531,8 @@ Truthness evaluateEquality(Object actualValue, Object expectedValue) { } else { return evaluateWithArrayUnwrapping(actualValue, value -> this.compareNullableValues( - expectedValue, - EQUALS_TO, value)); + value, EQUALS_TO, expectedValue + )); } } diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BsonHelper.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BsonHelper.java index d1f9994add..900171756f 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BsonHelper.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BsonHelper.java @@ -282,4 +282,5 @@ public static byte[] getBinaryData(Object value) { throw new RuntimeException(e); } } + } diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index bb5668eb55..0fb5febde7 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -2280,7 +2280,6 @@ public void testInWithARegexElement() { } @Test - @Disabled("two sub-documents are never equal to one another, so the query never matches") public void testEqualityBetweenTwoSubDocuments() { /* mongo: matches every one of these. A sub-document is compared field by field @@ -2308,6 +2307,75 @@ public void testEqualityBetweenTwoSubDocuments() { assertTrue(calculator.computeHeuristicDocument(implicitEquality, arrayOfSubDocuments).isTrue()); } + @Test + public void testEqualityBetweenTwoDocuments() { + Document doc = new Document().append("a", + new Document().append("x", 1).append("y", 2)); + Document query = new Document().append("a", new Document().append("$eq", + new Document().append("x", 1).append("y", 2))); + + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + assertTrue(calculator.computeHeuristicDocument(query, doc).isTrue()); + } + + @Test + public void testInEqualityBetweenTwoDocuments() { + Document doc = new Document().append("a", + new Document().append("x", 1).append("y", 2)); + Document query = new Document().append("a", new Document().append("$eq", + new Document().append("x", 1))); + + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + assertTrue(calculator.computeHeuristicDocument(query, doc).isFalse()); + } + + @Test + public void testLessThanInSubDocuments() { + Document doc = new Document().append("a", + new Document().append("x", 1)); + Document query = new Document().append("a", new Document().append("$lte", + new Document().append("x", 2))); + + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + assertTrue(calculator.computeHeuristicDocument(query, doc).isTrue()); + } + + @Test + public void testLessThanEqualInSubDocuments() { + Document doc = new Document().append("a", + new Document().append("x", 1)); + Document query = new Document().append("a", new Document().append("$lte", + new Document().append("x", 0))); + + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + assertTrue(calculator.computeHeuristicDocument(query, doc).isFalse()); + } + + + @Test + public void testLessThanEqualWithDifferentFields() { + Document doc = new Document().append("a", + new Document().append("x", 1)); + Document query = new Document().append("a", new Document().append("$lte", + new Document().append("y", 1))); + + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + assertTrue(calculator.computeHeuristicDocument(query, doc).isTrue()); + } + + + @Test + public void testLessThanEqualWithMoreFields() { + Document doc = new Document().append("a", + new Document().append("x", 1).append("y", 2)); + Document query = new Document().append("a", new Document().append("$lte", + new Document().append("x", 1))); + + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + assertTrue(calculator.computeHeuristicDocument(query, doc).isFalse()); + } + + @Test @Disabled("$ne and $nin report a match between two sub-documents that are equal") public void testInequalityBetweenTwoSubDocuments() { From 0797c2e4fa85ac1f87e9d31ed5f8a55c2efaaea1 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Fri, 18 Sep 2026 20:30:07 -0300 Subject: [PATCH 38/53] fix semantics for $eq, $lte, $gt, $gte, $ne of ObjectIds --- .../MongoHeuristicsCalculatorHelper.java | 51 +++++++++++-------- .../controller/mongo/utils/BsonHelper.java | 12 +++++ .../mongo/MongoHeuristicsCalculatorTest.java | 1 - 3 files changed, 42 insertions(+), 22 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java index b8c3d678fa..3da891f336 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java @@ -177,17 +177,16 @@ List compareDocuments(Object actualValue, ComparisonOperatorType op, Truthness compareNonNullValues(Object actualValue, ComparisonOperatorType op, Object expectedValue) { - - Objects.requireNonNull(expectedValue); Objects.requireNonNull(actualValue); + Objects.requireNonNull(expectedValue); final Truthness truthnessOfComparison; - if (expectedValue instanceof Number && actualValue instanceof Number) { + if (actualValue instanceof Number && expectedValue instanceof Number) { final Number expectedValueAsNumber = (Number) expectedValue; final Number actualNumberAsValue = (Number) actualValue; truthnessOfComparison = compareNumberValues(actualNumberAsValue, op, expectedValueAsNumber); - } else if (expectedValue instanceof String && actualValue instanceof String) { + } else if (actualValue instanceof String && expectedValue instanceof String) { String expectedValueAsString = (String) expectedValue; String actualValueAsString = (String) actualValue; if (taintHandler != null && op == EQUALS_TO) { @@ -196,46 +195,45 @@ Truthness compareNonNullValues(Object actualValue, ComparisonOperatorType op, Ob truthnessOfComparison = calculateTruthnessForStringComparison( actualValueAsString, expectedValueAsString, op); - } else if (expectedValue instanceof Boolean && actualValue instanceof Boolean) { + } else if (actualValue instanceof Boolean && expectedValue instanceof Boolean) { int expectedValueAsInt = toIntValue((Boolean) expectedValue); int actualValueAsInt = toIntValue((Boolean) actualValue); truthnessOfComparison = calculateTruthnessForNumberComparison( actualValueAsInt, expectedValueAsInt, op); - } else if (expectedValue instanceof List && actualValue instanceof List) { + } else if (actualValue instanceof List && expectedValue instanceof List) { final List expectedValueAsList = (List) expectedValue; final List actualValueAsList = (List) actualValue; truthnessOfComparison = compareNonNullLists(actualValueAsList, op, expectedValueAsList); - } else if (expectedValue instanceof Date && actualValue instanceof Date) { + } else if (actualValue instanceof Date && expectedValue instanceof Date) { final Instant expectedValueAsInstant = convertToInstant(expectedValue); final Instant actualValueAsInstant = convertToInstant(actualValue); truthnessOfComparison = calculateTruthnessForInstantComparison( actualValueAsInstant, expectedValueAsInstant, op); - } else if (BsonHelper.isBsonTimestamp(expectedValue) && BsonHelper.isBsonTimestamp(actualValue)) { + } else if (BsonHelper.isBsonTimestamp(actualValue) && BsonHelper.isBsonTimestamp(expectedValue)) { long expectedValueAsTimestampValue = BsonHelper.getBsonTimestampValue(expectedValue); long actualValueAsTimestampValue = BsonHelper.getBsonTimestampValue(actualValue); truthnessOfComparison = calculateTruthnessForNumberComparison(actualValueAsTimestampValue, expectedValueAsTimestampValue, op); - } else if (BsonHelper.isBsonRegularExpression(expectedValue) && BsonHelper.isBsonRegularExpression(actualValue)) { + } else if (BsonHelper.isBsonRegularExpression(actualValue) && BsonHelper.isBsonRegularExpression(expectedValue)) { String expectedValuePatternAsString = BsonHelper.bsonRegexGetPattern(expectedValue); String actualValuePatternAsString = BsonHelper.bsonRegexGetPattern(actualValue); truthnessOfComparison = calculateTruthnessForStringComparison(actualValuePatternAsString, expectedValuePatternAsString, op); - } else if (BsonHelper.isObjectId(expectedValue) || BsonHelper.isObjectId(actualValue)) { - String expectedValueAsString = expectedValue.toString(); - String actualValueAsString = actualValue.toString(); - if (taintHandler != null && op == EQUALS_TO) { - taintHandler.handleTaintForStringEquals(expectedValueAsString, actualValueAsString, false); - } - truthnessOfComparison = calculateTruthnessForStringComparison(actualValueAsString, expectedValueAsString, op); + } else if (BsonHelper.isObjectId(actualValue) && BsonHelper.isObjectId(expectedValue)) { - } else if (BsonHelper.isBsonBinary(expectedValue) && BsonHelper.isBsonBinary(actualValue)) { - byte[] expectedValueAsByteArray = BsonHelper.getBinaryData(expectedValue); - byte[] actualValueAsByteArray = BsonHelper.getBinaryData(actualValue); + byte[] actualValueAsByteArray = BsonHelper.toByteArray(actualValue); + byte[] expectedValueAsByteArray = BsonHelper.toByteArray(expectedValue); + truthnessOfComparison = compareBinaryData(actualValueAsByteArray, op, expectedValueAsByteArray); + + } else if (BsonHelper.isBsonBinary(actualValue) && BsonHelper.isBsonBinary(expectedValue)) { + byte[] actualValueAsByteArray = BsonHelper.getBinaryData(actualValue); + byte[] expectedValueAsByteArray = BsonHelper.getBinaryData(expectedValue); truthnessOfComparison = compareBinaryData(actualValueAsByteArray, op, expectedValueAsByteArray); + } else if (BsonHelper.isBsonDocument(expectedValue) && BsonHelper.isBsonDocument(actualValue)) { truthnessOfComparison = compareDocumentValues(actualValue, op, expectedValue); } else { @@ -244,6 +242,17 @@ Truthness compareNonNullValues(Object actualValue, ComparisonOperatorType op, Ob // only if the comparison operator is NOT_EQUALS_TO. Otherwise returns false. truthnessOfComparison = op == NOT_EQUALS_TO ? TRUE_C : C_FALSE; } + + /** + * Taint Analysis + */ + if (BsonHelper.isObjectId(actualValue) || BsonHelper.isObjectId(expectedValue)) { + String expectedValueAsString = expectedValue.toString(); + String actualValueAsString = actualValue.toString(); + if (taintHandler != null && op == EQUALS_TO) { + taintHandler.handleTaintForStringEquals(expectedValueAsString, actualValueAsString, false); + } + } return truthnessOfComparison; } @@ -262,9 +271,9 @@ private Truthness compareDocumentValues(Object actualValue, return buildAndAggregationTruthness(truthnesses.toArray(new Truthness[0])); } - private Truthness compareBinaryData(byte[] expectedValueAsByteArray, + private Truthness compareBinaryData(byte[] actualValueAsByteArray, ComparisonOperatorType op, - byte[] actualValueAsByteArray) { + byte[] expectedValueAsByteArray) { final Truthness equalityTruthness = getEqualityTruthness(actualValueAsByteArray, expectedValueAsByteArray); switch (op) { diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BsonHelper.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BsonHelper.java index 900171756f..a781f96af6 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BsonHelper.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BsonHelper.java @@ -27,6 +27,7 @@ public class BsonHelper { public static final String NULL_TYPE = "null"; public static final String BSON_TYPE_NULL = "NULL"; public static final String GET_DATA = "getData"; + public static final String TO_BYTE_ARRAY = "toByteArray"; public static Object newDocument(Object bsonDocument) { @@ -283,4 +284,15 @@ public static byte[] getBinaryData(Object value) { } } + public static byte[] toByteArray(Object actualValue) { + Objects.requireNonNull(actualValue, "The provided value cannot be null"); + if (!isObjectId(actualValue)) { + throw new IllegalArgumentException("The provided value is not a BSON ObjectId but class: " + actualValue.getClass().getName()); + } + try { + return (byte[]) actualValue.getClass().getMethod(TO_BYTE_ARRAY).invoke(actualValue); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } } diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 0fb5febde7..51503e6123 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -1985,7 +1985,6 @@ public void testOrderingComparisonWithNaN() { } @Test - @Disabled("an ObjectId is compared against any other value as a string, which orders it wrongly") public void testOrderingComparisonBetweenAnObjectIdAndANumber() { /* mongo: no match, an ObjectId and a number are of different BSON types and do From 640be3b35cc84472c343289f158ca5f00d9588cb Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Sat, 19 Sep 2026 08:36:37 -0300 Subject: [PATCH 39/53] removed old semantics test case --- .../client/java/controller/mongo/QueryParserTest.java | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java index 4492ba1e1d..4a3bfaffe4 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java @@ -1280,17 +1280,6 @@ void testParseInvalidEmptyOperatorDocument() { assertNull(operation); } - - @Test - void testParseInvalidLessEqualsThanNull() { - Document query = new Document( - "age", - new Document("$lte", null) - ); - QueryOperation operation = parser.parse(query); - assertNull(operation); - } - @Test void testParseInvalidMultipleOperatorsIncludingUnknownOperator() { Document query = new Document( From 8daaddd7ffb1902fec480f29b1e9acd556df869a Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Sat, 19 Sep 2026 08:39:06 -0300 Subject: [PATCH 40/53] restore disabled test case --- .../java/controller/mongo/MongoHeuristicsCalculatorTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 51503e6123..d5d3dde560 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -2376,7 +2376,6 @@ public void testLessThanEqualWithMoreFields() { @Test - @Disabled("$ne and $nin report a match between two sub-documents that are equal") public void testInequalityBetweenTwoSubDocuments() { /* mongo: no match for either, the sub-document held by the field is the one the From 42d72f5de20a4c7ee636594c0a3a75aa5898a7be Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Mon, 21 Sep 2026 12:23:21 -0300 Subject: [PATCH 41/53] support for $type operations with a list of types / support for "number" alias type name. --- .../mongo/MongoHeuristicsCalculator.java | 34 +++- .../mongo/operations/TypeOperation.java | 24 ++- .../mongo/selectors/TypeSelector.java | 154 +++++++++++++++++- .../controller/mongo/utils/BsonHelper.java | 35 ++-- .../mongo/MongoHeuristicsCalculatorTest.java | 7 +- .../controller/mongo/QueryParserTest.java | 88 ++++++++-- .../mongo/utils/BsonHelperTest.java | 13 +- 7 files changed, 310 insertions(+), 45 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java index 6f590c25c2..2f7b05f76c 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java @@ -27,6 +27,9 @@ public class MongoHeuristicsCalculator { + private static final String JAVA_UTIL_LIST = "java.util.List"; + private static final String NULL = "null"; + /** * A handler responsible for managing taint propagation and tracking * during the execution of heuristic calculations. This object is used @@ -551,15 +554,36 @@ private Truthness computeHeuristic(TypeOperation operation, Object document) { String fieldName = operation.getFieldName(); if (!documentContainsField(document, fieldName)) { + /** + * If the document does not contain the specified field, the $type operation cannot be satisfied. + * Even if the expected BSON type is "null", the absence of the field does not satisfy the condition, + * as the $type operator checks for the type of an existing field, not its absence. Therefore, + * the heuristic score is set to C_FALSE, indicating that the document does not meet the condition. + */ return C_FALSE; } else { - final Object bsonType = operation.getType(); - String expectedType = getType(bsonType); + final Object actualValue = getValue(document, fieldName); + final String actualTypeAsString; + if (actualValue != null && actualValue instanceof List) { + /** + * If the actual value is a List, we consider its type as "java.util.List" for the purpose of type comparison. + */ + actualTypeAsString = JAVA_UTIL_LIST; + } else { + actualTypeAsString = actualValue == null ? NULL : actualValue.getClass().getTypeName(); + } - Object actualValue = getValue(document, fieldName); - String actualType = actualValue == null ? "null" : actualValue.getClass().getTypeName(); + final List expectedBsonTypes = operation.getBsonTypes(); + final List truthnesses = new LinkedList<>(); + for (Object expectedBsonType : expectedBsonTypes) { + String expectedTypeAsString = getType(expectedBsonType); + final Truthness equalityTruthness = SqlExpressionEvaluator.getEqualityTruthness( + actualTypeAsString, + expectedTypeAsString); + truthnesses.add(equalityTruthness); + } - final Truthness equalityTruthness = SqlExpressionEvaluator.getEqualityTruthness(actualType, expectedType); + Truthness equalityTruthness = buildOrAggregationTruthness(truthnesses.toArray(new Truthness[0])); return buildSafeScaledTruthness(equalityTruthness); } } diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/operations/TypeOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/operations/TypeOperation.java index ae9570a404..624462ce5d 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/operations/TypeOperation.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/operations/TypeOperation.java @@ -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 bsonTypes; - public TypeOperation(String fieldName, Object type) { + public TypeOperation(String fieldName, List 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 getBsonTypes() { + return bsonTypes; } } diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/TypeSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/TypeSelector.java index 41f9902085..9fb17d9293 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/TypeSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/TypeSelector.java @@ -2,7 +2,11 @@ import org.evomaster.client.java.controller.mongo.operations.*; -import static org.evomaster.client.java.controller.mongo.utils.BsonHelper.getTypeFromAlias; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import static org.evomaster.client.java.controller.mongo.utils.BsonHelper.bsonTypeValueOf; import static org.evomaster.client.java.controller.mongo.utils.BsonHelper.getTypeFromNumber; /** @@ -12,16 +16,154 @@ public class TypeSelector extends SingleConditionQuerySelector { public static final String TYPE_OPERATOR = "$type"; + /** + * BSON type aliases for numeric types. The alias "number" is used to represent any numeric BSON type, + * including double, int, long, and decimal. + */ + private static final String NUMBER = "number"; + private static final String DOUBLE = "double"; + private static final String INT = "int"; + private static final String LONG = "long"; + private static final String DECIMAL = "decimal"; + private static final String STRING = "string"; + private static final String OBJECT = "object"; + private static final String ARRAY = "array"; + private static final String BIN_DATA = "binData"; + private static final String UNDEFINED = "undefined"; + private static final String OBJECT_ID = "objectId"; + private static final String BOOL = "bool"; + private static final String DATE = "date"; + private static final String NULL = "null"; + private static final String REGEX = "regex"; + private static final String DB_POINTER = "dbPointer"; + private static final String JAVASCRIPT = "javascript"; + private static final String SYMBOL = "symbol"; + private static final String JAVASCRIPT_WITH_SCOPE = "javascriptWithScope"; + private static final String TIMESTAMP = "timestamp"; + private static final String MIN_KEY = "minKey"; + private static final String MAX_KEY = "maxKey"; + @Override protected QueryOperation parseValue(String fieldName, Object value) { - Object type = null; - if (value instanceof Integer) { - type = getTypeFromNumber((Integer) value); + List parsedBsonTypes = new LinkedList<>(); + if (value instanceof List) { + List valueList = (List) value; + for (Object valueListElement : valueList) { + List bsonTypes = parseToBsonTypes(valueListElement); + if (bsonTypes == null) { + return null; + } + parsedBsonTypes.addAll(bsonTypes); + } + } else { + List bsonTypes = parseToBsonTypes(value); + if (bsonTypes == null) { + return null; + } + parsedBsonTypes.addAll(bsonTypes); + } + if (parsedBsonTypes.isEmpty()) { + return null; } + return new TypeOperation(fieldName, parsedBsonTypes); + } + + + /** + * Parses the given value to determine the corresponding BSON type(s). + * As the "number" alias is used to represent any numeric BSON type, + * a list of BsonTypes is returned instead of a single BsonType. + * + * @param value + * @return + */ + public static List parseToBsonTypes(Object value) { if (value instanceof String) { - type = getTypeFromAlias((String) value); + String alias = (String) value; + if (alias.equals(NUMBER)) { + /** + * The "number" alias is used to represent any numeric BSON type, including double, int, long, and decimal. + * Therefore, when the alias is "number", a list of all numeric BSON types is returned instead of a single BSON type. + * This allows for more flexible queries that can match any numeric type in the database, rather than being limited to a specific numeric type. + * The list of BSON types returned for the "number" alias includes DOUBLE, INT, LONG, and DECIMAL. + */ + List bsonTypes = new LinkedList<>(); + bsonTypes.addAll(parseToBsonTypes(DOUBLE)); + bsonTypes.addAll(parseToBsonTypes(INT)); + bsonTypes.addAll(parseToBsonTypes(LONG)); + bsonTypes.addAll(parseToBsonTypes(DECIMAL)); + return bsonTypes; + } else { + String enumName = getBsonTypeEnumNameFromAlias(alias); + if (enumName == null) { + return null; + } + final Object bsonType = bsonTypeValueOf(enumName); + if (bsonType == null) { + return null; + } + return Arrays.asList(bsonType); + } + } else if (value instanceof Number) { + final Object bsonType = getTypeFromNumber(((Number) value).intValue()); + if (bsonType == null) { + return null; + } + return Arrays.asList(bsonType); + } else { + return null; + } + } + + static String getBsonTypeEnumNameFromAlias(String alias) { + + switch (alias) { + case DOUBLE: + return "DOUBLE"; + case STRING: + return "STRING"; + case OBJECT: + return "DOCUMENT"; + case ARRAY: + return "ARRAY"; + case BIN_DATA: + return "BINARY"; + case UNDEFINED: + return "UNDEFINED"; + case OBJECT_ID: + return "OBJECT_ID"; + case BOOL: + return "BOOLEAN"; + case DATE: + return "DATE_TIME"; + case NULL: + return "NULL"; + case REGEX: + return "REGULAR_EXPRESSION"; + case DB_POINTER: + return "DB_POINTER"; + case JAVASCRIPT: + return "JAVASCRIPT"; + case SYMBOL: + return "SYMBOL"; + case JAVASCRIPT_WITH_SCOPE: + return "JAVASCRIPT_WITH_SCOPE"; + case INT: + return "INT32"; + case TIMESTAMP: + return "TIMESTAMP"; + case LONG: + return "INT64"; + case DECIMAL: + return "DECIMAL128"; + case MIN_KEY: + return "MIN_KEY"; + case MAX_KEY: + return "MAX_KEY"; + default: + // If the alias is not recognized, return null to indicate an invalid BSON alias name. + return null; } - return type == null ? null : new TypeOperation(fieldName, type); } @Override diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BsonHelper.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BsonHelper.java index a781f96af6..94a8f2452e 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BsonHelper.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BsonHelper.java @@ -116,7 +116,7 @@ public static Boolean isBsonDocument(Object value) { * @return true if the object is a BSON ObjectId, false otherwise */ public static boolean isObjectId(Object obj) { - return obj!=null && obj.getClass().getName().equals(ORG_BSON_TYPES_OBJECT_ID); + return obj != null && obj.getClass().getName().equals(ORG_BSON_TYPES_OBJECT_ID); } /** @@ -129,6 +129,16 @@ public static boolean isBsonTimestamp(Object obj) { return obj != null && obj.getClass().getName().equals(ORG_BSON_BSON_TIMESTAMP); } + /** + * Determines whether the given object is a BSON type. + * + * @param obj the object to check; should be non-null to determine if it is a BSON type + * @return true if the object is a BSON type, false otherwise + */ + public static boolean isBsonType(Object obj) { + return (obj != null) && obj.getClass().getName().equals(ORG_BSON_BSON_TYPE); + } + /** * Retrieves the value of a BSON BsonTimestamp as a long value. * @@ -192,18 +202,21 @@ public static Object getTypeFromNumber(Integer number) { /** * Retrieves the BSON type corresponding to the given alias string. * - * @param alias the alias string representing the BSON type + * @param name the string representing the BSON type * @return the BSON type object, or null if not found */ - public static Object getTypeFromAlias(String alias) { + public static Object bsonTypeValueOf(String name) { Class bsonTypeClass; try { bsonTypeClass = Class.forName(ORG_BSON_BSON_TYPE); - Method valueOf = bsonTypeClass.getMethod(VALUE_OF_METHOD, String.class); - return valueOf.invoke(null, alias); - } catch (ClassNotFoundException | IllegalAccessException | InvocationTargetException | - NoSuchMethodException e) { - return null; + if (bsonTypeClass.isEnum()) { + Enum bsonTypeEnum = Enum.valueOf((Class) bsonTypeClass, name); + return bsonTypeEnum; + } else { + throw new IllegalArgumentException("BSON type expected to be an enum but is not. Class: " + bsonTypeClass.getName()); + } + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); } } @@ -222,9 +235,9 @@ public static boolean isBsonRegularExpression(Object object) { * * @param value the object representing a BSON regular expression. Must not be null and must be a valid BSON regular expression. * @return a String representing the pattern of the BSON regular expression. - * @throws NullPointerException if the provided value is null. + * @throws NullPointerException if the provided value is null. * @throws IllegalArgumentException if the provided value is not a BSON regular expression. - * @throws RuntimeException if an error occurs while invoking the method to retrieve the pattern. + * @throws RuntimeException if an error occurs while invoking the method to retrieve the pattern. */ public static String bsonRegexGetPattern(Object value) { Objects.requireNonNull(value, "The provided value cannot be null"); @@ -243,7 +256,7 @@ public static String bsonRegexGetPattern(Object value) { * * @param value the object representing a BSON regular expression. Must not be null and must be a valid BSON regular expression. * @return a String representing the options associated with the BSON regular expression. - * @throws NullPointerException if the provided value is null. + * @throws NullPointerException if the provided value is null. * @throws IllegalArgumentException if the provided value is not a BSON regular expression. **/ public static String bsonRegexGetOptions(Object value) { diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index d5d3dde560..7a175e59c0 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -709,10 +709,9 @@ public void testTypeExplicitVersion() { @Test public void testTypeAliasVersion() { - // This is not exactly the alias. Should be? Document doc = new Document().append("age", 20); - Bson bsonTrue = Filters.type("age", BsonType.INT32.name()); - Bson bsonFalse = Filters.type("age", BsonType.DOUBLE.name()); + Bson bsonTrue = Filters.type("age", "int"); + Bson bsonFalse = Filters.type("age", "double"); Truthness distanceMatch = new MongoHeuristicsCalculator().computeHeuristicDocument(convertToDocument(bsonTrue), doc); Truthness distanceNotMatch = new MongoHeuristicsCalculator().computeHeuristicDocument(convertToDocument(bsonFalse), doc); assertTrue(distanceMatch.isTrue()); @@ -1687,7 +1686,6 @@ public void testNotEqualsAgainstAnArrayField() { */ @Test - @Disabled("$type with a string alias is not parsed, so the calculator throws a NullPointerException") public void testTypeWithAStringAlias() { /* mongo: {"a": {"$type": "string"}} matches {a:'abc'} and not {a:5}. @@ -2015,7 +2013,6 @@ public void testNullInsideAnArrayField() { } @Test - @Disabled("$type with a list of aliases is not parsed, so the calculator throws a NullPointerException") public void testTypeWithAListOfAliases() { // mongo: matches, the field holds one of the listed types Document query = new Document().append("a", diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java index 4a3bfaffe4..140c716484 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java @@ -10,13 +10,11 @@ import org.bson.types.Binary; import org.bson.types.Decimal128; import org.evomaster.client.java.controller.mongo.operations.*; +import org.evomaster.client.java.controller.mongo.selectors.TypeSelector; import org.evomaster.client.java.controller.mongo.utils.BitmaskUtils; import org.junit.jupiter.api.Test; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.OptionalLong; +import java.util.*; import java.util.regex.Pattern; import static org.junit.jupiter.api.Assertions.*; @@ -360,15 +358,28 @@ void testParseElemMatchWithOrCondition() { void testParseType() { Document query = new Document( "name", - new Document("$type", "STRING") + new Document("$type", "string") ); QueryOperation operation = parser.parse(query); assertTrue(operation instanceof TypeOperation); TypeOperation type = (TypeOperation) operation; assertEquals("name", type.getFieldName()); - assertNotNull(type.getType()); + assertNotNull(type.getBsonTypes()); + List expectedBsonTypes = TypeSelector.parseToBsonTypes("string"); + assertEquals(expectedBsonTypes, type.getBsonTypes()); } + @Test + void testParseInvalidType() { + Document query = new Document( + "name", + new Document("$type", "STRING") + ); + QueryOperation operation = parser.parse(query); + assertNull(operation); + } + + @Test void testParseTypeWithNumber() { Document query = new Document( @@ -379,7 +390,7 @@ void testParseTypeWithNumber() { assertTrue(operation instanceof TypeOperation); TypeOperation type = (TypeOperation) operation; assertEquals("name", type.getFieldName()); - assertNotNull(type.getType()); + assertNotNull(type.getBsonTypes()); } @Test @@ -1633,6 +1644,64 @@ void testParseElemMatchWithLessThanEqualsOperator() { assertEquals("b", lessThanEquals.getValue()); } + @Test + void testParseTypeWithListOfTypes() { + // mongo: matches, the field holds one of the listed types + Document query = new Document().append("a", + new Document().append("$type", Arrays.asList("string", "double"))); + + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof TypeOperation); + TypeOperation type = (TypeOperation) operation; + + final List expectedBsonTypes = new LinkedList<>(); + expectedBsonTypes.addAll(TypeSelector.parseToBsonTypes("string")); + expectedBsonTypes.addAll(TypeSelector.parseToBsonTypes("double")); + assertEquals(expectedBsonTypes, type.getBsonTypes()); + } + + @Test + void testParseAllTypes() { + // mongo: matches, the field holds one of the listed types + final List aliases = Arrays.asList( + "double", + "string", + "object", + "array", + "binData", + "undefined", + "objectId", + "bool", + "date", + "null", + "regex", + "dbPointer", + "javascript", + "symbol", + "javascriptWithScope", + "int", + "timestamp", + "long", + "decimal", + "minKey", + "maxKey", + "number" + ); + Document query = new Document().append("a", + new Document().append("$type", + aliases)); + + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof TypeOperation); + TypeOperation type = (TypeOperation) operation; + + final List expectedBsonTypes = new LinkedList<>(); + for (String alias : aliases) { + expectedBsonTypes.addAll(TypeSelector.parseToBsonTypes(alias)); + } + assertEquals(expectedBsonTypes, type.getBsonTypes()); + } + @Test void testParseElemMatchWithInOperator() { ElemMatchOperation elemMatch = parseElemMatchCondition( @@ -1901,7 +1970,7 @@ void testParseInvalidNorWithInvalidCondition() { @Test void testBinaryBitmask() { - Binary mask = new Binary(new byte[] { 0x30 }); // 0011 0000: bits 4 and 5 + Binary mask = new Binary(new byte[]{0x30}); // 0011 0000: bits 4 and 5 Document query = new Document( "flags", @@ -1918,7 +1987,7 @@ void testBinaryBitmask() { @Test void testBsonBinaryBitmask() { - BsonBinary mask = new BsonBinary(new byte[] { 0x30 }); // 0011 0000: bits 4 and 5 + BsonBinary mask = new BsonBinary(new byte[]{0x30}); // 0011 0000: bits 4 and 5 Document query = new Document( "flags", @@ -1986,7 +2055,6 @@ void testParseLesserThanOrEqualNull() { } - private ElemMatchOperation parseElemMatchCondition( Document condition, Class expectedConditionType) { diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/BsonHelperTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/BsonHelperTest.java index 51744afcaf..af24f2e732 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/BsonHelperTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/BsonHelperTest.java @@ -122,14 +122,14 @@ void testGetTypeFromNumberReturnsNullWhenNotFound() { @Test void testGetTypeFromAlias() { - Object bsonType = BsonHelper.getTypeFromAlias("STRING"); - + // aliases are case-insensitive + Object bsonType = BsonHelper.bsonTypeValueOf("string"); assertEquals(BsonType.STRING, bsonType); } @Test void testGetTypeFromAliasReturnsNullWhenNotFound() { - assertNull(BsonHelper.getTypeFromAlias("UNKNOWN")); + assertNull(BsonHelper.bsonTypeValueOf("UNKNOWN")); } @Test @@ -222,4 +222,11 @@ void testGetBinaryData() { assertArrayEquals(data, BsonHelper.getBinaryData(binary)); } + + @Test + void testIsBsonType() { + assertTrue(BsonHelper.isBsonType(BsonType.INT32)); + assertFalse(BsonHelper.isBsonType("not a BsonType")); + assertFalse(BsonHelper.isBsonType(null)); + } } From 747e5f1bdf20c9d287b343ea039444e8fd05c1f7 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Mon, 21 Sep 2026 16:14:47 -0300 Subject: [PATCH 42/53] fix test cases --- .../java/controller/mongo/utils/BsonHelperTest.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/BsonHelperTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/BsonHelperTest.java index af24f2e732..2db04e6fd5 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/BsonHelperTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/BsonHelperTest.java @@ -123,13 +123,18 @@ void testGetTypeFromNumberReturnsNullWhenNotFound() { @Test void testGetTypeFromAlias() { // aliases are case-insensitive - Object bsonType = BsonHelper.bsonTypeValueOf("string"); + Object bsonType = BsonHelper.bsonTypeValueOf("STRING"); assertEquals(BsonType.STRING, bsonType); } @Test void testGetTypeFromAliasReturnsNullWhenNotFound() { - assertNull(BsonHelper.bsonTypeValueOf("UNKNOWN")); + try { + BsonHelper.bsonTypeValueOf("UNKNOWN"); + fail(); + } catch (IllegalArgumentException e) { + + } } @Test From aff379ada058694f9a0e621e4a982e52c4bc9471 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Tue, 22 Sep 2026 13:52:04 -0300 Subject: [PATCH 43/53] fix parsing of MongoDB queries. Now negative/fractional bitmasks are rejected. --- .../controller/mongo/utils/BitmaskUtils.java | 49 ++++++++++++++++--- .../controller/mongo/utils/BsonHelper.java | 36 ++++++++++++-- .../mongo/MongoHeuristicsCalculatorTest.java | 45 ----------------- .../controller/mongo/QueryParserTest.java | 32 ++++++++++++ .../mongo/utils/BitmaskUtilsTest.java | 32 ++++++++++++ .../mongo/utils/BsonHelperTest.java | 15 ++++++ 6 files changed, 153 insertions(+), 56 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BitmaskUtils.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BitmaskUtils.java index e431959a2f..65746aca0d 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BitmaskUtils.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BitmaskUtils.java @@ -1,6 +1,9 @@ package org.evomaster.client.java.controller.mongo.utils; +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.List; +import java.util.Objects; import java.util.OptionalLong; public abstract class BitmaskUtils { @@ -10,19 +13,23 @@ public abstract class BitmaskUtils { * The input value can be of the following types: * - A {@code Number}, which is directly converted to its {@code long} value. * - A {@code List} of numbers where each number represents a bit position. - * These positions are used to generate a bitmask by setting the corresponding bits. + * These positions are used to generate a bitmask by setting the corresponding bits. * - A {@code byte[]} where the bytes represent the binary value of the bitmask. - * Only the least significant 64 bits are considered; higher-order bits must be zero. + * Only the least significant 64 bits are considered; higher-order bits must be zero. * * @param value the input value to convert. Must be a {@code Number}, a {@code List} of numbers, or a {@code byte[]} * that satisfies the described conditions. * @return an {@code OptionalLong} containing the resulting bitmask if the input value can be converted successfully; - * otherwise, an empty {@code OptionalLong}. + * otherwise, an empty {@code OptionalLong}. */ public static OptionalLong toBitMaskValue(Object value) { // value can be a number - if (value instanceof Number) { - return OptionalLong.of(((Number) value).longValue()); + if (value instanceof Number && !hasFractionalPart((Number) value)) { + final long longvalue = ((Number) value).longValue(); + if (longvalue < 0) { + return OptionalLong.empty(); // negative value — no fit + } + return OptionalLong.of(longvalue); } // value can be a list of positions if (value instanceof List) { @@ -30,7 +37,7 @@ public static OptionalLong toBitMaskValue(Object value) { for (Object p : (List) value) { if (p instanceof Number) { long pos = ((Number) p).longValue(); - if (pos >= Long.SIZE) { + if (pos < 0 || pos >= Long.SIZE) { return OptionalLong.empty(); // position 64+ — no fit } mask |= 1L << pos; // pos 63 -> Long.MIN_VALUE, fine @@ -64,4 +71,34 @@ private static OptionalLong toBitMaskValue0(byte[] bytes) { } return OptionalLong.of(mask); } + + static boolean hasFractionalPart(Number n) { + Objects.requireNonNull(n, "n"); + + if (n instanceof BigDecimal) { + return ((BigDecimal) n).stripTrailingZeros().scale() > 0; + } + + if (BsonHelper.isDecimal128(n)) { + BigDecimal bigDecimal = BsonHelper.getBigDecimalValue(n); + return bigDecimal.stripTrailingZeros().scale() > 0; + } + + if (n instanceof BigInteger || + n instanceof Byte || n instanceof Short || + n instanceof Integer || n instanceof Long) { + return false; + } + + if (n instanceof Float || n instanceof Double) { + double d = n.doubleValue(); + if (!Double.isFinite(d)) { + throw new IllegalArgumentException("NaN and infinity are not finite numbers"); + } + return d % 1 != 0; + } + + throw new IllegalArgumentException( + "Unsupported Number implementation: " + n.getClass().getName()); + } } diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BsonHelper.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BsonHelper.java index 94a8f2452e..71aff44f0c 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BsonHelper.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BsonHelper.java @@ -2,6 +2,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.math.BigDecimal; import java.util.Objects; import java.util.Set; @@ -18,16 +19,18 @@ public class BsonHelper { private static final String GET_PATTERN_METHOD = "getPattern"; private static final String GET_OPTIONS_METHOD = "getOptions"; - public static final String ORG_BSON_BSON_BINARY = "org.bson.BsonBinary"; + private static final String ORG_BSON_BSON_BINARY = "org.bson.BsonBinary"; private static final String ORG_BSON_BSON_TYPE = "org.bson.BsonType"; private static final String BSON_REGEX_CLASS = "org.bson.BsonRegularExpression"; private static final String ORG_BSON_DOCUMENT = "org.bson.Document"; - public static final String ORG_BSON_TYPES_BINARY = "org.bson.types.Binary"; + private static final String ORG_BSON_TYPES_BINARY = "org.bson.types.Binary"; + private static final String ORG_BSON_TYPES_DECIMAL_128 = "org.bson.types.Decimal128"; public static final String NULL_TYPE = "null"; - public static final String BSON_TYPE_NULL = "NULL"; - public static final String GET_DATA = "getData"; - public static final String TO_BYTE_ARRAY = "toByteArray"; + private static final String BSON_TYPE_NULL = "NULL"; + private static final String GET_DATA = "getData"; + private static final String TO_BYTE_ARRAY = "toByteArray"; + private static final String BIG_DECIMAL_VALUE = "bigDecimalValue"; public static Object newDocument(Object bsonDocument) { @@ -308,4 +311,27 @@ public static byte[] toByteArray(Object actualValue) { throw new RuntimeException(e); } } + + public static boolean isDecimal128(Number n) { + return n!=null && n.getClass().getName().equals(ORG_BSON_TYPES_DECIMAL_128); + } + + public static BigDecimal getBigDecimalValue(Number n) { + Objects.requireNonNull(n, "n"); + + if (!isDecimal128(n)) { + throw new IllegalArgumentException("The provided number is not a BSON Decimal128 but class: " + n.getClass().getName()); + } + try { + Method getValueMethod = n.getClass().getMethod(BIG_DECIMAL_VALUE); + Object decimalValue = getValueMethod.invoke(n); + if (decimalValue instanceof BigDecimal) { + return (BigDecimal) decimalValue; + } else { + throw new IllegalStateException("Expected BigDecimal value but got: " + decimalValue.getClass().getName()); + } + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } } diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 7a175e59c0..20ebcbdb06 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -2563,51 +2563,6 @@ public void testExistsGivenAValueThatIsNotABoolean() { assertTrue(assertDoesNotThrow(() -> calculator.computeHeuristicDocument(nullValue, doc)).isFalse()); } - @Test - @Disabled("a negative bit position is read as bit 63 instead of being rejected") - public void testBitmaskWithANegativeBitPosition() { - /* - mongo: "Failed to parse bit position. Expected a non-negative number in: 0: -1". - - A shift count is masked to its low six bits in Java, so 1L << -1 sets bit 63 - and the mask becomes Long.MIN_VALUE, which the value below then matches. - Rejecting the mask on its own is not enough here: if the mask stops parsing, - the selector answers null and the query goes unparsed, which is the throwing - case rather than a fix. - */ - Document doc = new Document().append("a", Long.MIN_VALUE); - MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); - - Document query = new Document().append("a", - new Document().append("$bitsAllSet", Collections.singletonList(-1))); - Truthness truthness = assertDoesNotThrow(() -> calculator.computeHeuristicDocument(query, doc)); - assertTrue(truthness.isFalse()); - } - - @Test - @Disabled("a bitmask that is not an integer is truncated instead of being rejected") - public void testBitmaskThatIsNotAnInteger() { - // mongo: "Expected an integer: $bitsAllSet: 3.9". Here 3.9 is read as the mask 3. - Document doc = new Document().append("a", 3); - MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); - - Document query = new Document().append("a", new Document().append("$bitsAllSet", 3.9d)); - Truthness truthness = assertDoesNotThrow(() -> calculator.computeHeuristicDocument(query, doc)); - assertTrue(truthness.isFalse()); - } - - @Test - @Disabled("a negative bitmask is read as all ones instead of being rejected") - public void testBitmaskThatIsNegative() { - // mongo: "Expected a non-negative number in: $bitsAllSet: -1" - Document doc = new Document().append("a", -1); - MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); - - Document query = new Document().append("a", new Document().append("$bitsAllSet", -1)); - Truthness truthness = assertDoesNotThrow(() -> calculator.computeHeuristicDocument(query, doc)); - assertTrue(truthness.isFalse()); - } - @Test @Disabled("the documents are traversed more than once, so a one-shot Iterable is scored wrongly") public void testDocumentsThatCanOnlyBeTraversedOnce() { diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java index 140c716484..d492b4d723 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java @@ -664,6 +664,16 @@ void testParseBitsAllSetInteger() { assertEquals(5L, bitsAllSet.getBitmask()); } + @Test + void testParseInvalidFractionalBitmask() { + Document query = new Document( + "flags", + new Document("$bitsAllSet", 3.5d) + ); + QueryOperation operation = parser.parse(query); + assertNull(operation); + } + @Test void testParseBitsAnyClearLong() { Document query = new Document( @@ -1985,6 +1995,28 @@ void testBinaryBitmask() { assertEquals(expectedBitmask.getAsLong(), bitsAllSet.getBitmask()); } + @Test + void testNegativeBitmask() { + Document query = new Document( + "flags", + new Document("$bitsAllSet", -1) + ); + + QueryOperation operation = parser.parse(query); + assertNull(operation); + } + + @Test + void testNegativeArrayBitmask() { + Document query = new Document( + "flags", + new Document("$bitsAllSet", Arrays.asList(-1)) + ); + + QueryOperation operation = parser.parse(query); + assertNull(operation); + } + @Test void testBsonBinaryBitmask() { BsonBinary mask = new BsonBinary(new byte[]{0x30}); // 0011 0000: bits 4 and 5 diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/BitmaskUtilsTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/BitmaskUtilsTest.java index 46b914688f..c57ffd934f 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/BitmaskUtilsTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/BitmaskUtilsTest.java @@ -63,4 +63,36 @@ void testToBitMaskValueWithUnsupportedType() { ); } + @Test + void testToBitMaskValueWithNegativePosition() { + assertEquals(OptionalLong.empty(), BitmaskUtils.toBitMaskValue(Arrays.asList(-1, 0, 1))); + } + + @Test + void testToBitMaskValueWithNegative() { + assertEquals(OptionalLong.empty(), BitmaskUtils.toBitMaskValue(-1)); + } + + @Test + void testToBitMaskValueWithFractionalNumber() { + assertEquals(OptionalLong.empty(), BitmaskUtils.toBitMaskValue(3.5d)); + } + + @Test + void testToBitMaskValueWithNoFractionalNumber() { + assertEquals(OptionalLong.of(3L), BitmaskUtils.toBitMaskValue(3.0d)); + } + + @Test + void testHasFractionalPart() { + assertTrue(BitmaskUtils.hasFractionalPart(3.5d)); + assertFalse(BitmaskUtils.hasFractionalPart(3.0d)); + assertFalse(BitmaskUtils.hasFractionalPart(3L)); + assertFalse(BitmaskUtils.hasFractionalPart(new BigDecimal("3.0"))); + assertTrue(BitmaskUtils.hasFractionalPart(new BigDecimal("3.5"))); + assertTrue(BitmaskUtils.hasFractionalPart(3.5f)); + assertFalse(BitmaskUtils.hasFractionalPart(3.0f)); + assertTrue(BitmaskUtils.hasFractionalPart(new Decimal128(new BigDecimal("3.5")))); + assertFalse(BitmaskUtils.hasFractionalPart(new Decimal128(new BigDecimal("3.0")))); + } } diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/BsonHelperTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/BsonHelperTest.java index 2db04e6fd5..6f21674d01 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/BsonHelperTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/BsonHelperTest.java @@ -14,6 +14,7 @@ import org.bson.types.ObjectId; import org.bson.types.Symbol; +import java.math.BigDecimal; import java.util.Arrays; import java.util.Date; import java.util.HashSet; @@ -234,4 +235,18 @@ void testIsBsonType() { assertFalse(BsonHelper.isBsonType("not a BsonType")); assertFalse(BsonHelper.isBsonType(null)); } + + @Test + void testIsDecimal128() { + assertTrue(BsonHelper.isDecimal128(new Decimal128(1234567890123456789L))); + assertFalse(BsonHelper.isDecimal128(123.45)); + assertFalse(BsonHelper.isDecimal128(null)); + } + + @Test + void testGetBigDecimalValue() { + Decimal128 decimal128 = new Decimal128(1234567890123456789L); + final BigDecimal expectedBigDecimalValue = decimal128.bigDecimalValue(); + assertEquals(expectedBigDecimalValue, BsonHelper.getBigDecimalValue(decimal128)); + } } From 5d15c36bd390f24fd408fa529526c3a5c1c46bf2 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Tue, 22 Sep 2026 15:52:10 -0300 Subject: [PATCH 44/53] fix parsing of $exists queries for non-boolean values (e.g. falsy and truthy values) --- .../mongo/selectors/ExistsSelector.java | 51 +++++++++++++-- .../controller/mongo/utils/BsonHelper.java | 64 +++++++++++++++---- .../mongo/MongoHeuristicsCalculatorTest.java | 1 - .../controller/mongo/QueryParserTest.java | 6 +- 4 files changed, 103 insertions(+), 19 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ExistsSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ExistsSelector.java index 708cf75984..c5f315bcb1 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ExistsSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ExistsSelector.java @@ -1,7 +1,11 @@ package org.evomaster.client.java.controller.mongo.selectors; import org.evomaster.client.java.controller.mongo.operations.*; +import org.evomaster.client.java.controller.mongo.utils.BsonHelper; +import org.evomaster.client.java.controller.problem.rpc.schema.params.BigDecimalParam; +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.Objects; /** @@ -15,15 +19,52 @@ public class ExistsSelector extends SingleConditionQuerySelector { protected QueryOperation parseValue(String fieldName, Object value) { Objects.requireNonNull(fieldName); - if (value!=null && value instanceof Boolean) { - return new ExistsOperation(fieldName, (Boolean) value); - } else { - return null; - } + boolean truthy = isTruthy(value); + return new ExistsOperation(fieldName, truthy); } @Override protected String operator() { return EXISTS_OPERATOR; } + + static boolean isTruthy(Object v) { + if (v == null) { + return false; // null / missing field + } + + if (BsonHelper.isBsonUndefined(v)) { + return false; + } + + if (v instanceof Boolean) { + return (Boolean) v; + } + + if (v instanceof Number && BsonHelper.isDecimal128(v)) { + // NaN and Infinity are non-zero, so truthy + if (BsonHelper.isNaN(v) || BsonHelper.isInfinite(v)) { + return true; + } + BigDecimal d = BsonHelper.getBigDecimalValue((Number) v); + return d.signum() != 0; + } + + if (v instanceof BigDecimal) { + BigDecimal d = (BigDecimal) v; + return d.signum() != 0; + } + + if (v instanceof BigInteger) { + BigInteger i = (BigInteger) v; + return i.signum() != 0; + } + + if (v instanceof Number) { + Number n = (Number) v; + return n.doubleValue() != 0.0; // NaN != 0.0 → true + } + + return true; // strings (incl. ""), lists (incl. []), maps, dates, ObjectId, ... + } } diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BsonHelper.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BsonHelper.java index 71aff44f0c..5996399c05 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BsonHelper.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BsonHelper.java @@ -15,9 +15,11 @@ public class BsonHelper { private static final String GET_TYPE_NAME_METHOD = "getTypeName"; private static final String GET_VALUE_METHOD = "getValue"; private static final String FIND_BY_VALUE_METHOD = "findByValue"; - private static final String VALUE_OF_METHOD = "valueOf"; private static final String GET_PATTERN_METHOD = "getPattern"; private static final String GET_OPTIONS_METHOD = "getOptions"; + private static final String GET_DATA_METHOD = "getData"; + private static final String TO_BYTE_ARRAY_METHOD = "toByteArray"; + private static final String BIG_DECIMAL_VALUE_METHOD = "bigDecimalValue"; private static final String ORG_BSON_BSON_BINARY = "org.bson.BsonBinary"; private static final String ORG_BSON_BSON_TYPE = "org.bson.BsonType"; @@ -25,13 +27,11 @@ public class BsonHelper { private static final String ORG_BSON_DOCUMENT = "org.bson.Document"; private static final String ORG_BSON_TYPES_BINARY = "org.bson.types.Binary"; private static final String ORG_BSON_TYPES_DECIMAL_128 = "org.bson.types.Decimal128"; + private static final String ORG_BSON_BSON_UNDEFINED = "org.bson.BsonUndefined"; + private static final String ORG_BSON_TYPES_UNDEFINED = "org.bson.types.Undefined"; public static final String NULL_TYPE = "null"; private static final String BSON_TYPE_NULL = "NULL"; - private static final String GET_DATA = "getData"; - private static final String TO_BYTE_ARRAY = "toByteArray"; - private static final String BIG_DECIMAL_VALUE = "bigDecimalValue"; - public static Object newDocument(Object bsonDocument) { Objects.requireNonNull(bsonDocument); @@ -268,7 +268,8 @@ public static String bsonRegexGetOptions(Object value) { throw new IllegalArgumentException("The provided value is not a BSON regular expression but class: " + value.getClass().getName()); } try { - return (String) value.getClass().getMethod(GET_OPTIONS_METHOD).invoke(value); + final Method getOptionsMethod = value.getClass().getMethod(GET_OPTIONS_METHOD); + return (String) getOptionsMethod.invoke(value); } catch (ReflectiveOperationException e) { throw new RuntimeException(e); } @@ -294,7 +295,8 @@ public static byte[] getBinaryData(Object value) { throw new IllegalArgumentException("The provided value is not a BSON binary type but class: " + value.getClass().getName()); } try { - return (byte[]) value.getClass().getMethod(GET_DATA).invoke(value); + final Method getDataMethod = value.getClass().getMethod(GET_DATA_METHOD); + return (byte[]) getDataMethod.invoke(value); } catch (ReflectiveOperationException e) { throw new RuntimeException(e); } @@ -306,14 +308,15 @@ public static byte[] toByteArray(Object actualValue) { throw new IllegalArgumentException("The provided value is not a BSON ObjectId but class: " + actualValue.getClass().getName()); } try { - return (byte[]) actualValue.getClass().getMethod(TO_BYTE_ARRAY).invoke(actualValue); + final Method toByteArrayMethod = actualValue.getClass().getMethod(TO_BYTE_ARRAY_METHOD); + return (byte[]) toByteArrayMethod.invoke(actualValue); } catch (ReflectiveOperationException e) { throw new RuntimeException(e); } } - public static boolean isDecimal128(Number n) { - return n!=null && n.getClass().getName().equals(ORG_BSON_TYPES_DECIMAL_128); + public static boolean isDecimal128(Object v) { + return v!=null && v.getClass().getName().equals(ORG_BSON_TYPES_DECIMAL_128); } public static BigDecimal getBigDecimalValue(Number n) { @@ -323,8 +326,8 @@ public static BigDecimal getBigDecimalValue(Number n) { throw new IllegalArgumentException("The provided number is not a BSON Decimal128 but class: " + n.getClass().getName()); } try { - Method getValueMethod = n.getClass().getMethod(BIG_DECIMAL_VALUE); - Object decimalValue = getValueMethod.invoke(n); + Method bigDecimalValueMethod = n.getClass().getMethod(BIG_DECIMAL_VALUE_METHOD); + Object decimalValue = bigDecimalValueMethod.invoke(n); if (decimalValue instanceof BigDecimal) { return (BigDecimal) decimalValue; } else { @@ -334,4 +337,41 @@ public static BigDecimal getBigDecimalValue(Number n) { throw new RuntimeException(e); } } + + public static boolean isBsonUndefined(Object v) { + if (v == null) { + return false; + } + String className = v.getClass().getName(); + return className.equals(ORG_BSON_BSON_UNDEFINED) || className.equals(ORG_BSON_TYPES_UNDEFINED); + } + + public static boolean isNaN(Object v) { + Objects.requireNonNull(v); + if (!isDecimal128(v)) { + throw new IllegalArgumentException("The provided value is not a BSON Decimal128 but class: " + v.getClass().getName()); + } + try { + Method isNaNMethod = v.getClass().getMethod("isNaN"); + boolean result = (boolean) isNaNMethod.invoke(v); + return result; + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + public static boolean isInfinite(Object v) { + Objects.requireNonNull(v); + if (!isDecimal128(v)) { + throw new IllegalArgumentException("The provided value is not a BSON Decimal128 but class: " + v.getClass().getName()); + } + try { + Method isInfiniteMethod = v.getClass().getMethod("isInfinite"); + boolean result = (boolean) isInfiniteMethod.invoke(v); + return result; + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + } diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 20ebcbdb06..2190be8051 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -2541,7 +2541,6 @@ public void testOrderingComparisonsGivenBothNullValues() { @Test - @Disabled("$exists given anything but a boolean is not parsed, so the calculator throws") public void testExistsGivenAValueThatIsNotABoolean() { /* mongo: takes any value for $exists and reads it as a truth value, so 1 and a diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java index d492b4d723..7de3243662 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java @@ -1321,7 +1321,11 @@ void testParseInvalidExistsNull() { new Document("$exists", null) ); QueryOperation operation = parser.parse(query); - assertNull(operation); + assertNotNull(operation); + assertTrue(operation instanceof ExistsOperation); + ExistsOperation exists = (ExistsOperation) operation; + assertEquals("age", exists.getFieldName()); + assertFalse(exists.getBoolean()); } @Test From 4c20654ba0a7145faedf3f21e47d907bd5310f05 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Tue, 22 Sep 2026 17:21:52 -0300 Subject: [PATCH 45/53] traverse documents only once (allows OneShotIterable) --- .../mongo/MongoHeuristicsCalculator.java | 46 ++++++++----------- .../mongo/MongoHeuristicsCalculatorTest.java | 1 - 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java index 2f7b05f76c..c1539baa9c 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java @@ -26,7 +26,6 @@ */ public class MongoHeuristicsCalculator { - private static final String JAVA_UTIL_LIST = "java.util.List"; private static final String NULL = "null"; @@ -57,39 +56,32 @@ public MongoHeuristicsCalculator(TaintHandler taintHandler) { public MongoDistanceWithMetrics computeDistanceDocuments(Object query, Iterable documents) { - long count = StreamSupport.stream(documents.spliterator(), false).count(); - Truthness heuristicScoreCollection = getTruthnessToEmpty((int) count).invert(); - QueryOperation queryOperation = parseQuery(query); - Truthness hCondition = computeHeuristicOnDocuments(queryOperation, documents); - - Truthness hQuery = buildAndAggregationTruthness(heuristicScoreCollection, hCondition); - - // Map truthness to distance where 0 is true. - // If it's true, distance 0. - // If it's false, distance is 1.0 - ofTrue. - double distance = hQuery.isTrue() ? 0.0 : 1.0 - hQuery.getOfTrue(); - - return new MongoDistanceWithMetrics(distance, (int) count); - } - private Truthness computeHeuristicOnDocuments(QueryOperation operation, Iterable documents) { - long count = StreamSupport.stream(documents.spliterator(), false).count(); - if (count == 0) { - return C_FALSE; - } - - double maxOfTrue = 0; - boolean first = true; + double maxOfTrue = 0d; + boolean isFirstDocument = true; + int documentCount = 0; for (Object doc : documents) { - double ofTrue = computeHeuristicQueryOperation(operation, doc).getOfTrue(); - if (first || ofTrue > maxOfTrue) { + double ofTrue = computeHeuristicQueryOperation(queryOperation, doc).getOfTrue(); + if (isFirstDocument || ofTrue > maxOfTrue) { maxOfTrue = ofTrue; } - first = false; + isFirstDocument = false; + documentCount++; } - return buildSafeScaledTruthness(maxOfTrue); + Truthness heuristicScoreCollection = documentCount>0 ? TRUE_C : C_FALSE; + Truthness hCondition = buildSafeScaledTruthness(maxOfTrue); + + Truthness hQuery = buildAndAggregationTruthness( + heuristicScoreCollection, + hCondition); + + // Map truthness to distance where 0 is true. + // If it's true, distance 0. + // If it's false, distance is 1.0 - ofTrue. + double distance = hQuery.isTrue() ? 0.0 : 1.0 - hQuery.getOfTrue(); + return new MongoDistanceWithMetrics(distance, documentCount); } /** diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 2190be8051..27d66e1c97 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -2563,7 +2563,6 @@ public void testExistsGivenAValueThatIsNotABoolean() { } @Test - @Disabled("the documents are traversed more than once, so a one-shot Iterable is scored wrongly") public void testDocumentsThatCanOnlyBeTraversedOnce() { /* Not a question of MongoDB semantics but of the contract of the calculator. From ed359383692b78aedfcec3af31ad5abdf9951b12 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Tue, 22 Sep 2026 19:09:58 -0300 Subject: [PATCH 46/53] fixed issue with $comment --- .../java/controller/mongo/QueryParser.java | 40 ++++++++++++++++++- .../mongo/selectors/ImplicitSelector.java | 7 ++++ .../mongo/MongoHeuristicsCalculatorTest.java | 1 - .../controller/mongo/QueryParserTest.java | 32 ++++++++++++++- 4 files changed, 76 insertions(+), 4 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/QueryParser.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/QueryParser.java index 50083defe2..6e333ca55c 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/QueryParser.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/QueryParser.java @@ -21,7 +21,10 @@ public class QueryParser { private static final String SYNTHETIC_FIELD_NAME = "$"; - private static final Set COMMENTS_OPERATORS = new HashSet<>(Arrays.asList("$comment", "$comments")); + 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 COMMENTS_OPERATORS = new HashSet<>(Arrays.asList("$comments")); List selectors = Arrays.asList( new EqualsSelector(), @@ -57,7 +60,18 @@ public QueryOperation parse(Object bsonDocument) { return null; } - Object normalizedWithoutComments = removeCommentsOperators(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)) { @@ -71,6 +85,28 @@ public QueryOperation parse(Object bsonDocument) { return parseWithSelectors(normalizedDocument); } + private boolean isOnlyNoiseComments(Object bsonDocument) { + if (!isBsonDocument(bsonDocument)) { + return false; + } + Set 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); diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ImplicitSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ImplicitSelector.java index 377f28f6c9..5639ed98ce 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ImplicitSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ImplicitSelector.java @@ -18,6 +18,7 @@ public class ImplicitSelector extends QuerySelector { private static final String PREFIX_OPERATOR = "$"; + private static final String COMMENT_OPERATOR = "$comment"; @Override public QueryOperation getOperation(Object query) { @@ -70,6 +71,12 @@ private QueryOperation handleSingleField(Object query) { if (isEmptyDocument(value)) { return null; } + // "$comment" is not a recognized field-level operator: a value that is solely + // { $comment: ... } is treated as a literal document to match, not as an operator. + Set innerKeys = documentKeys(value); + if (innerKeys != null && innerKeys.size() == 1 && innerKeys.contains(COMMENT_OPERATOR)) { + return new EqualsOperation<>(fieldName, value); + } QueryOperation multiOperatorOp = handlePotentialMultiOperatorValue(query, fieldName, value); if (multiOperatorOp != null || isBsonDocumentWithOperators(value)) { return multiOperatorOp; diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 27d66e1c97..707a713d9b 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -2200,7 +2200,6 @@ public void testNotWithABareRegex() { } @Test - @Disabled("$comment is removed from documents that are values rather than operators") public void testCommentInsideALiteralValue() { /* mongo: matches. A field name that starts with "$" is storable since 5.0, and diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java index 7de3243662..55a9e293c2 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java @@ -1448,12 +1448,30 @@ void testParseAttachedComments() { assertEquals(42, eq.getValue()); } + @Test + void testParseTopLevelDollar() { + Document query = new Document("$foo", "bar"); + QueryOperation operation = parser.parse(query); + assertNull(operation); + } + @Test void testParseOnlyComments() { Document query = new Document("$comments", "a comment"); QueryOperation operation = parser.parse(query); + assertNull(operation); + } - assertTrue(operation instanceof EmptyOperation); + @Test + void testParseQueryWithComment() { + Document query = new Document().append("a", 1).append("$comment", "note"); + + QueryOperation operation = parser.parse(query); + assertNotNull(operation); + assertTrue(operation instanceof EqualsOperation); + EqualsOperation eq = (EqualsOperation) operation; + assertEquals("a", eq.getFieldName()); + assertEquals(1, eq.getValue()); } @Test @@ -1464,6 +1482,18 @@ void testParseOnlyComment() { assertTrue(operation instanceof EmptyOperation); } + @Test + void testCommentAsField() { + Document query = new Document("foo" ,new Document("$comment", "bar")); + QueryOperation operation = parser.parse(query); + + assertNotNull(operation); + assertTrue(operation instanceof EqualsOperation); + EqualsOperation eq = (EqualsOperation) operation; + assertEquals("foo", eq.getFieldName()); + assertEquals(new Document("$comment", "bar"), eq.getValue()); + } + @Test void testParseAndWithOnlyCommentElement() { Document query = new Document( From 92efc621eb87150ffdc1e2a1e8038325c59d352a Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Tue, 22 Sep 2026 19:30:05 -0300 Subject: [PATCH 47/53] an $in operator matches a regex in the expected value --- .../MongoHeuristicsCalculatorHelper.java | 14 ++++++++-- .../mongo/operations/RegexOptions.java | 27 +++++++++++++++++++ .../mongo/selectors/RegexSelector.java | 26 +----------------- .../mongo/MongoHeuristicsCalculatorTest.java | 1 - 4 files changed, 40 insertions(+), 28 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java index 3da891f336..4151d1da36 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java @@ -2,6 +2,7 @@ import org.evomaster.client.java.controller.mongo.geometry.GeoJsonPoint; import org.evomaster.client.java.controller.mongo.geometry.GeoJsonUtils; +import org.evomaster.client.java.controller.mongo.operations.RegexOptions; import org.evomaster.client.java.controller.mongo.utils.BsonHelper; import org.evomaster.client.java.controller.mongo.utils.MongoUtils; import org.evomaster.client.java.distance.heuristics.Truthness; @@ -365,9 +366,18 @@ Truthness computeHeuristicContainsElement(Object element, List list) { return C_FALSE; } else { Truthness res = buildOrAggregationTruthness(list.stream() - .map(expectedValue -> compareNullableValues(element, EQUALS_TO, expectedValue - )) + .map(expectedValue -> { + if (BsonHelper.isBsonRegularExpression(expectedValue)) { + String expectedPattern = BsonHelper.bsonRegexGetPattern(expectedValue); + String expectedOptions = BsonHelper.bsonRegexGetOptions(expectedValue); + Pattern pattern = Pattern.compile(expectedPattern); + return evaluateRegularExpression(element.toString(), pattern, taintHandler); + } else { + return compareNullableValues(element, EQUALS_TO, expectedValue); + } + }) .toArray(Truthness[]::new)); + return buildSafeScaledTruthness(res); } } diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/operations/RegexOptions.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/operations/RegexOptions.java index 9b30e3e383..51b496b9da 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/operations/RegexOptions.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/operations/RegexOptions.java @@ -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. @@ -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; + } + + } diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/RegexSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/RegexSelector.java index 284cb8aad7..b4b77bf7a4 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/RegexSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/RegexSelector.java @@ -113,7 +113,7 @@ public static RegexOperation buildRegexOperation(String fieldName, String patter final Pattern compile; try { - final int flags = javaRegexFlagsFromOptions(parsedOptions); + final int flags = parsedOptions.toJavaRegexFlags(); compile = Pattern.compile(pattern, flags); } catch (PatternSyntaxException e) { return null; @@ -136,30 +136,6 @@ private static RegexOptions parseOptions(String options) { options.indexOf(MONGO_REGEX_OPTION_UNICODE) >= 0); } - private static int javaRegexFlagsFromOptions(RegexOptions options) { - int flags = 0; - if (options.isCaseInsensitive()) { - flags |= Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE; - } - if (options.isMultiline()) { - flags |= Pattern.MULTILINE; - } - if (options.isDotAll()) { - flags |= Pattern.DOTALL; - } - if (options.isExtended()) { - flags |= Pattern.COMMENTS; - } - if (options.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; - } - private String optionsFromFlags(int flags) { StringBuilder options = new StringBuilder(); if ((flags & Pattern.CASE_INSENSITIVE) != 0) options.append(MONGO_REGEX_OPTION_CASE_INSENSITIVE); diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 707a713d9b..2b7a5a185d 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -2263,7 +2263,6 @@ public void testAllWithASingleNullAgainstAMissingField() { } @Test - @Disabled("$in does not match an element of the list that is a regex") public void testInWithARegexElement() { // mongo: matches, $in accepts regexes among its elements Document doc = new Document().append("a", "xy"); From be05a101d97d7f93bee039c09ab4b1a09f9a5591 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Tue, 22 Sep 2026 19:37:16 -0300 Subject: [PATCH 48/53] fix $elemMatch within $all condition --- .../mongo/MongoHeuristicsCalculator.java | 5 +-- .../mongo/selectors/AllSelector.java | 36 ++++++++++++++++++- .../mongo/MongoHeuristicsCalculatorTest.java | 1 - 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java index c1539baa9c..dc75b5e895 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java @@ -384,8 +384,9 @@ private Truthness evaluate(AllOperation operation, Object actualValue) { Truthness truthness = buildAndAggregationTruthness( expectedValues.stream() - .map(expectedElementValue -> - helper.evaluateEquality(actualValue, expectedElementValue)) + .map(expectedElementValue -> expectedElementValue instanceof ElemMatchOperation + ? evaluate((ElemMatchOperation) expectedElementValue, actualValue) + : helper.evaluateEquality(actualValue, expectedElementValue)) .toArray(Truthness[]::new)); return truthness; diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/AllSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/AllSelector.java index 80fa06bd01..1aeada1fa9 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/AllSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/AllSelector.java @@ -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. @@ -13,10 +20,14 @@ * 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) { @@ -24,12 +35,35 @@ protected QueryOperation parseValue(String fieldName, Object value) { Objects.requireNonNull(value); if (value instanceof List) { - return new AllOperation<>(fieldName, (List) value); + List 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 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; diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 2b7a5a185d..3a18d5405e 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -2442,7 +2442,6 @@ public void testComparisonBetweenTwoBinaryValues() { } @Test - @Disabled("$all holding an $elemMatch is compared as a value instead of applied as a condition") public void testAllHoldingAnElemMatch() { /* mongo: matches both. $all accepts $elemMatch among its elements, and then every From 5bbf6bb4dd661d0c0776bcefedf02cd620ccf7fd Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Tue, 22 Sep 2026 19:44:47 -0300 Subject: [PATCH 49/53] added test case for $elemMatch within $all --- .../controller/mongo/QueryParserTest.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java index 55a9e293c2..c5a615c2c1 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java @@ -23,6 +23,26 @@ class QueryParserTest { private final QueryParser parser = new QueryParser(); + @Test + void testParseElemMatchWithinAll() { + // { "a": { "$all": [ { "$elemMatch": { "$gt": 2 } } ] } } + Document query = new Document().append("a", new Document().append("$all", + Collections.singletonList(new Document().append("$elemMatch", + new Document().append("$gt", 2))))); + QueryOperation operation = parser.parse(query); + assertNotNull(operation); + assertTrue(operation instanceof AllOperation); + AllOperation allOperation = (AllOperation) operation; + assertEquals("a", allOperation.getFieldName()); + assertEquals(1, allOperation.getValues().size()); + Object elemMatchValue = allOperation.getValues().get(0); + assertTrue(elemMatchValue instanceof ElemMatchOperation); + ElemMatchOperation elemMatchOperation = (ElemMatchOperation) elemMatchValue; + assertEquals("a", elemMatchOperation.getFieldName()); + assertTrue(elemMatchOperation.getCondition() instanceof GreaterThanOperation); + assertEquals(2, ((GreaterThanOperation) elemMatchOperation.getCondition()).getValue()); + } + @Test void testParseEquals() { Document query = new Document( From d0a58bde9aca6b8682a78528e3e6758b511528d3 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Tue, 22 Sep 2026 19:46:07 -0300 Subject: [PATCH 50/53] removed comments --- .../mongo/MongoHeuristicsCalculatorTest.java | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java index 3a18d5405e..197981ba83 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorTest.java @@ -1603,24 +1603,6 @@ public static Document convertToDocument(Bson filter) { return documentCodec.decode(bsonDocument.asBsonReader(), DecoderContext.builder().build()); } - - /* - ================================================================================ - MongoDB semantics: behaviour that is currently not reproduced by the calculator. - - Every expected value below was obtained by running the same query and the same - document against a real MongoDB 7.0.40 server, so the assertions state what the - database actually does rather than an interpretation of the documentation. - - The @Disabled tests fail today. Removing the annotation is all that is needed - once the corresponding behaviour is implemented, and they are deliberately one - per defect so they can be enabled independently, in any order. - - First, a few tests of behaviour that is already correct, as a guard while the - heuristic is being changed. - ================================================================================ - */ - @Test public void testOrderingComparisonsDoNotMatchAcrossIncomparableTypes() { // mongo: {value:42} is matched by none of these, a number and a string do not compare @@ -2123,18 +2105,6 @@ public void testNotWithMoreThanOneInnerOperator() { assertTrue(calculator.computeHeuristicDocument(nested, new Document("a", 5)).isTrue()); } - /* - ================================================================================ - Further cases, in the same form: every expected value comes from a MongoDB - 7.0.41 server answering that query against that document, one test per defect, - and @Disabled on the ones that fail today. - - Where the database refuses the query outright there is no answer to copy, so - those tests assert that the calculator answers false without throwing, that - being the only answer available to it. - ================================================================================ - */ - @Test public void testRegexAgainstAnArrayWithoutStringElements() { /* From 61ac4d32eee09683a18782faf4be20b831c3c0d8 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Wed, 23 Sep 2026 09:49:24 -0300 Subject: [PATCH 51/53] specify visibility and documentation --- .../MongoHeuristicsCalculatorHelper.java | 134 +++++++++++++++--- 1 file changed, 113 insertions(+), 21 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java index 4151d1da36..574101a88a 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java @@ -42,7 +42,7 @@ public MongoHeuristicsCalculatorHelper(TaintHandler taintHandler) { this.taintHandler = taintHandler; } - static Truthness buildSafeScaledTruthness(double maxOfTrue) { + public static Truthness buildSafeScaledTruthness(double maxOfTrue) { if (maxOfTrue == 1.0) { return TRUE_C; } else { @@ -66,7 +66,7 @@ static Truthness buildSafeScaledTruthness(double maxOfTrue) { * where one component (true or false) is 1 to reflect the match status, and the other * captures the approximation in cases of non-exact matches */ - static Truthness evaluateRegularExpression(String actualValue, Pattern pattern, TaintHandler taintHandler) { + public static Truthness evaluateRegularExpression(String actualValue, Pattern pattern, TaintHandler taintHandler) { final String patternString = pattern.pattern(); if (taintHandler != null) { @@ -92,17 +92,17 @@ static Truthness evaluateRegularExpression(String actualValue, Pattern pattern, } } - static int toIntValue(Boolean actualValue) { + private static int toIntValue(Boolean actualValue) { return actualValue ? 1 : 0; } - static Truthness buildSafeScaledTruthness(Truthness truthness) { + public static Truthness buildSafeScaledTruthness(Truthness truthness) { Objects.requireNonNull(truthness); return buildSafeScaledTruthness(truthness.getOfTrue()); } - Truthness compareNonNullLists(List actualValueAsList, ComparisonOperatorType op, List expectedValueAsList) { + private Truthness compareNonNullLists(List actualValueAsList, ComparisonOperatorType op, List expectedValueAsList) { Objects.requireNonNull(expectedValueAsList); Objects.requireNonNull(actualValueAsList); @@ -118,7 +118,7 @@ Truthness compareNonNullLists(List actualValueAsList, ComparisonOperatorType } } - Truthness evaluateListEquality(List actualList, List expectedList) { + private Truthness evaluateListEquality(List actualList, List expectedList) { if (actualList.size() != expectedList.size()) { return C_FALSE; @@ -137,7 +137,7 @@ Truthness evaluateListEquality(List actualList, List expectedList) { return truthness; } - List compareDocuments(Object actualValue, ComparisonOperatorType op, Object expectedValue) { + private List compareDocuments(Object actualValue, ComparisonOperatorType op, Object expectedValue) { Objects.requireNonNull(actualValue); Objects.requireNonNull(op); @@ -177,7 +177,15 @@ List compareDocuments(Object actualValue, ComparisonOperatorType op, } - Truthness compareNonNullValues(Object actualValue, ComparisonOperatorType op, Object expectedValue) { + /** + * Compares two non-null values using the specified comparison operator. + * + * @param actualValue the actual value to compare, which must not be null + * @param op the comparison operator to use + * @param expectedValue the expected value to compare against, which must not be null + * @return a Truthness object representing the result of the comparison + */ + public Truthness compareNonNullValues(Object actualValue, ComparisonOperatorType op, Object expectedValue) { Objects.requireNonNull(actualValue); Objects.requireNonNull(expectedValue); @@ -293,7 +301,7 @@ private Truthness compareBinaryData(byte[] actualValueAsByteArray, } } - static Truthness compareNumberValues(Number actualValueAsNumber, ComparisonOperatorType op, Number expectedValueAsNumber) { + private static Truthness compareNumberValues(Number actualValueAsNumber, ComparisonOperatorType op, Number expectedValueAsNumber) { Objects.requireNonNull(expectedValueAsNumber); Objects.requireNonNull(op); Objects.requireNonNull(actualValueAsNumber); @@ -328,7 +336,15 @@ static Truthness compareNumberValues(Number actualValueAsNumber, ComparisonOpera } } - Truthness compareNullableValues(Object actualValue, ComparisonOperatorType op, Object expectedValue) { + /** + * Compares two nullable values using the specified comparison operator. + * + * @param actualValue the actual value to compare, which can be null + * @param op the comparison operator to use + * @param expectedValue the expected value to compare against, which can be null + * @return a Truthness object representing the result of the comparison + */ + public Truthness compareNullableValues(Object actualValue, ComparisonOperatorType op, Object expectedValue) { if (expectedValue == null || actualValue == null) { switch (op) { case MINOR_THAN_EQUALS: @@ -359,7 +375,7 @@ Truthness compareNullableValues(Object actualValue, ComparisonOperatorType op, O * @param list the list of objects to search; must not be null. * @return a Truthness object representing the heuristic score of the element's presence in the list. */ - Truthness computeHeuristicContainsElement(Object element, List list) { + public Truthness computeHeuristicContainsElement(Object element, List list) { Objects.requireNonNull(list); if (list.isEmpty()) { @@ -382,7 +398,16 @@ Truthness computeHeuristicContainsElement(Object element, List list) { } } - Truthness computeHeuristicInOperation(Object actualValue, List expectedValueList) { + /** + * Computes a heuristic truthness score for the "$in" operation, evaluating whether the actual value + * is contained within the expected value list. If the actual value is a list, each element is compared + * against the expected value list, and the results are aggregated. + * + * @param actualValue the actual value to compare, which can be a single value or a list of values + * @param expectedValueList the list of expected values to compare against + * @return a Truthness object representing the result of the "in" operation evaluation + */ + public Truthness computeHeuristicInOperation(Object actualValue, List expectedValueList) { final Truthness res; if (actualValue instanceof List) { List actualValueList = (List) actualValue; @@ -419,18 +444,38 @@ Truthness computeHeuristicInOperation(Object actualValue, List expectedValueL return res; } - Truthness evaluateMod(Object actualValue, long divisor, long expectedRemainder) { + /** + * Evaluates the modulus operation on the actual value and compares it to the expected remainder. + * The method checks if the actual value is a number, computes the remainder of the division + * by the specified expectedDivisor, and then compares it to the expected remainder. The result is + * returned as a Truthness object, which captures both the definitive match result and the closeness to a potential match. + * + * @param actualValue the actual value to evaluate, which must be a Number + * @param expectedDivisor the divisor to use for the modulus operation + * @param expectedRemainder the expected remainder to compare against + * @return a Truthness object representing the result of the evaluation + */ + public Truthness evaluateMod(Object actualValue, long expectedDivisor, long expectedRemainder) { if (!(actualValue instanceof Number)) { return C_FALSE; } - long actualRemainder = ((Number) actualValue).longValue() % divisor; + final long actualValueAsLong = ((Number) actualValue).longValue(); + final long actualRemainder = actualValueAsLong % expectedDivisor; Truthness res = getEqualityTruthness(actualRemainder, expectedRemainder); return buildSafeScaledTruthness(res); } - Truthness evaluateBitsAllClearOperation(Object actualValue, long bitmask) { + /** + * Evaluates the "$bitsAllClear" operation, checking if all bits specified by the bitmask are clear + * in the actual value. + * + * @param actualValue the actual value to evaluate, which must be a Number + * @param bitmask the bitmask specifying which bits to check + * @return a Truthness object representing the result of the evaluation + */ + public Truthness evaluateBitsAllClearOperation(Object actualValue, long bitmask) { if (!(actualValue instanceof Number)) { return C_FALSE; } @@ -447,7 +492,14 @@ Truthness evaluateBitsAllClearOperation(Object actualValue, long bitmask) { return buildSafeScaledTruthness(equalityTruthness); } - Truthness evaluateBitsAllSetOperation(Object actualValue, long bitmask) { + /** + * Evaluates the "$bitsAllSet" operation, checking if all bits specified by the bitmask are set in the actual value. + * + * @param actualValue the actual value to evaluate, which must be a Number + * @param bitmask the bitmask specifying which bits to check + * @return a Truthness object representing the result of the evaluation + */ + public Truthness evaluateBitsAllSetOperation(Object actualValue, long bitmask) { if (!(actualValue instanceof Number)) { return C_FALSE; @@ -466,7 +518,14 @@ Truthness evaluateBitsAllSetOperation(Object actualValue, long bitmask) { return buildSafeScaledTruthness(equalityTruthness); } - Truthness evaluateBitsAnyClearOperation(Object actualValue, long bitmask) { + /** + * Evaluates the "$bitsAnyClear" operation, checking if any bits specified by the bitmask are clear in the actual value. + * + * @param actualValue the actual value to evaluate, which must be a Number + * @param bitmask the bitmask specifying which bits to check + * @return a Truthness object representing the result of the evaluation + */ + public Truthness evaluateBitsAnyClearOperation(Object actualValue, long bitmask) { if (!(actualValue instanceof Number)) { return C_FALSE; @@ -485,7 +544,14 @@ Truthness evaluateBitsAnyClearOperation(Object actualValue, long bitmask) { return buildSafeScaledTruthness(lessThanTruthness); } - Truthness evaluateBitsAnySetOperation(Object actualValue, long bitmask) { + /** + * Evaluates the "$bitsAnySet" operation, checking if any bits specified by the bitmask are set in the actual value. + * + * @param actualValue the actual value to evaluate, which must be a Number + * @param bitmask the bitmask specifying which bits to check + * @return a Truthness object representing the result of the evaluation + */ + public Truthness evaluateBitsAnySetOperation(Object actualValue, long bitmask) { if (!(actualValue instanceof Number)) { return C_FALSE; @@ -503,7 +569,23 @@ Truthness evaluateBitsAnySetOperation(Object actualValue, long bitmask) { return buildSafeScaledTruthness(lessThanTruthness); } - static Truthness evaluateDistanceBetweenPoints(Object actualValue, double minDistance, + /** + * Evaluates the distance between two geographical points and checks if it falls within a specified range. + * The method calculates the distance between the point represented by the given longitude and latitude + * and the point represented by the actualValue, which is expected to be a GeoJSON Point. + * The distance is then compared against the provided minimum and maximum distance thresholds. + * The result is returned as a Truthness object, which captures both the definitive match result and + * the closeness to a potential match. + * + * @param actualValue the actual value to evaluate, which must be a GeoJSON Point to represent a geographical location + * @param minDistance the minimum distance threshold + * @param maxDistance the maximum distance threshold + * @param longitude the longitude of the reference point + * @param latitude the latitude of the reference point + * @param geoSpatialModel the model to use for calculating the distance + * @return a Truthness object representing the result of the evaluation + */ + public static Truthness evaluateDistanceBetweenPoints(Object actualValue, double minDistance, double maxDistance, double longitude, double latitude, @@ -543,7 +625,17 @@ static Truthness evaluateDistanceBetweenPoints(Object actualValue, double minDis : getEqualityTruthness(distanceBetweenPoints, minDistance); } - Truthness evaluateEquality(Object actualValue, Object expectedValue) { + /** + * Evaluates the equality of two values, considering the possibility that either value may be a list. + * If the top-level comparison indicates equality, it returns that result. Otherwise, it attempts + * to unwrap the actual value if it is a list and compare each element against the expected + * value, aggregating the results to determine if any element matches. + * + * @param actualValue the actual value to compare, which can be a single value or a list of values + * @param expectedValue the expected value to compare against + * @return a Truthness object representing the result of the equality evaluation + */ + public Truthness evaluateEquality(Object actualValue, Object expectedValue) { Truthness topLevelTruthness = this.compareNullableValues(actualValue, EQUALS_TO, expectedValue); if (topLevelTruthness.isTrue()) { return topLevelTruthness; @@ -564,7 +656,7 @@ Truthness evaluateEquality(Object actualValue, Object expectedValue) { * @param elementHeuristic a function to compute the heuristic for each element or the single actualValue * @return a Truthness object representing the heuristic of the evaluated input */ - Truthness evaluateWithArrayUnwrapping(Object actualValue, + public Truthness evaluateWithArrayUnwrapping(Object actualValue, Function elementHeuristic) { Objects.requireNonNull(elementHeuristic); From c61e044133f0caa00dcdf4c671972e5248d15f52 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Wed, 23 Sep 2026 09:57:49 -0300 Subject: [PATCH 52/53] refactor moved utils to TruthnessUtils --- .../mongo/MongoHeuristicsCalculator.java | 1 - .../MongoHeuristicsCalculatorHelper.java | 23 --------------- .../distance/heuristics/TruthnessUtils.java | 28 +++++++++++++++++++ .../heuristics/TruthnessUtilsTest.java | 16 +++++++++++ 4 files changed, 44 insertions(+), 24 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java index dc75b5e895..7bcd3fe23f 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java @@ -15,7 +15,6 @@ import java.util.*; import java.util.regex.Pattern; -import java.util.stream.StreamSupport; /** * The MongoHeuristicsCalculator class provides methods to compute heuristic scores for MongoDB-like diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java index 574101a88a..abbb1fb697 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculatorHelper.java @@ -2,13 +2,10 @@ import org.evomaster.client.java.controller.mongo.geometry.GeoJsonPoint; import org.evomaster.client.java.controller.mongo.geometry.GeoJsonUtils; -import org.evomaster.client.java.controller.mongo.operations.RegexOptions; import org.evomaster.client.java.controller.mongo.utils.BsonHelper; import org.evomaster.client.java.controller.mongo.utils.MongoUtils; import org.evomaster.client.java.distance.heuristics.Truthness; -import org.evomaster.client.java.distance.heuristics.TruthnessUtils; import org.evomaster.client.java.instrumentation.coverage.methodreplacement.RegexDistanceUtils; -import org.evomaster.client.java.sql.heuristic.SqlExpressionEvaluator; import org.evomaster.client.java.sql.internal.TaintHandler; import java.time.Instant; @@ -30,26 +27,12 @@ public class MongoHeuristicsCalculatorHelper { - // TODO these constants should be replaced by DistanceHelper constants - public static final double C = 0.1; - public static final Truthness C_FALSE = new Truthness(C, 1.0); - // TODO These constants should be refactored by TruthnessUtils constants - public static final Truthness TRUE_C = new Truthness(1.0, C); - private final TaintHandler taintHandler; public MongoHeuristicsCalculatorHelper(TaintHandler taintHandler) { this.taintHandler = taintHandler; } - public static Truthness buildSafeScaledTruthness(double maxOfTrue) { - if (maxOfTrue == 1.0) { - return TRUE_C; - } else { - return buildScaledTruthness(C, maxOfTrue); - } - } - /** * Evaluates whether the given input string matches the provided regular expression pattern. @@ -96,12 +79,6 @@ private static int toIntValue(Boolean actualValue) { return actualValue ? 1 : 0; } - public static Truthness buildSafeScaledTruthness(Truthness truthness) { - Objects.requireNonNull(truthness); - - return buildSafeScaledTruthness(truthness.getOfTrue()); - } - private Truthness compareNonNullLists(List actualValueAsList, ComparisonOperatorType op, List expectedValueAsList) { Objects.requireNonNull(expectedValueAsList); diff --git a/client-java/distance-heuristics/src/main/java/org/evomaster/client/java/distance/heuristics/TruthnessUtils.java b/client-java/distance-heuristics/src/main/java/org/evomaster/client/java/distance/heuristics/TruthnessUtils.java index fdfdcc4709..5cd7267ce2 100644 --- a/client-java/distance-heuristics/src/main/java/org/evomaster/client/java/distance/heuristics/TruthnessUtils.java +++ b/client-java/distance-heuristics/src/main/java/org/evomaster/client/java/distance/heuristics/TruthnessUtils.java @@ -22,6 +22,21 @@ public class TruthnessUtils { */ public static final Truthness FALSE_TRUTHNESS_BETTER = new Truthness(DistanceHelper.H_NOT_NULL_BETTER, 1); + /** + * A constant value used for scaling truthness values. + */ + public static final double C = 0.1; + + /** + * A {@link Truthness} representing a condition that is fully satisfied with a scaling factor C. + */ + public static final Truthness TRUE_C = new Truthness(1.0, C); + + /** + * A {@link Truthness} representing a condition that is not satisfied with a scaling factor C. + */ + public static final Truthness C_FALSE = new Truthness(C, 1.0); + /** * Scales to a positive double value to the [0,1] range * @@ -393,4 +408,17 @@ public static Truthness getStringEqualityTruthness(String a, String b) { return new Truthness(ofTrue, 1d); } + public static Truthness buildSafeScaledTruthness(double maxOfTrue) { + if (maxOfTrue == 1.0) { + return TRUE_C; + } else { + return buildScaledTruthness(C, maxOfTrue); + } + } + + public static Truthness buildSafeScaledTruthness(Truthness truthness) { + Objects.requireNonNull(truthness); + + return buildSafeScaledTruthness(truthness.getOfTrue()); + } } diff --git a/client-java/distance-heuristics/src/test/java/org/evomaster/client/java/distance/heuristics/TruthnessUtilsTest.java b/client-java/distance-heuristics/src/test/java/org/evomaster/client/java/distance/heuristics/TruthnessUtilsTest.java index 8f2de329c9..6efc4b83d4 100644 --- a/client-java/distance-heuristics/src/test/java/org/evomaster/client/java/distance/heuristics/TruthnessUtilsTest.java +++ b/client-java/distance-heuristics/src/test/java/org/evomaster/client/java/distance/heuristics/TruthnessUtilsTest.java @@ -80,4 +80,20 @@ public void testGetEqualityTruthnessByteArrays() { assertThrows(NullPointerException.class, () -> TruthnessUtils.getEqualityTruthness(messiPhoto, null)); } + + @Test + public void testBuildSafeScaledTruthnessOfDouble() { + Truthness t = TruthnessUtils.buildSafeScaledTruthness(1.0d); + assertEquals(TruthnessUtils.TRUE_C, t); + } + + @Test + public void testBuildSafeScaledTruthnessOfTruthness() { + Truthness arg = new Truthness(0.5d, 1.0d); + Truthness rv = TruthnessUtils.buildSafeScaledTruthness(arg); + Truthness expected = TruthnessUtils.buildScaledTruthness(TruthnessUtils.C, 0.5d); + assertEquals(expected.getOfTrue(), rv.getOfTrue()); + assertEquals(expected.getOfFalse(), rv.getOfFalse()); + } + } From d4e9c775982b3f4b00e383d9f2498fc7a9a5487c Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Wed, 23 Sep 2026 11:33:01 -0300 Subject: [PATCH 53/53] fix test case --- .../client/java/controller/mongo/utils/BitmaskUtilsTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/BitmaskUtilsTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/BitmaskUtilsTest.java index c57ffd934f..e961184ecb 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/BitmaskUtilsTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/utils/BitmaskUtilsTest.java @@ -16,7 +16,7 @@ class BitmaskUtilsTest { void testToBitMaskValueWithNumber() { assertAll( () -> assertEquals(OptionalLong.of(5L), BitmaskUtils.toBitMaskValue(5)), - () -> assertEquals(OptionalLong.of(7L), BitmaskUtils.toBitMaskValue(7.9d)), + () -> assertEquals(OptionalLong.empty(), BitmaskUtils.toBitMaskValue(7.9d)), () -> assertEquals(OptionalLong.of(9L), BitmaskUtils.toBitMaskValue(9L)), () -> assertEquals(OptionalLong.of(11L), BitmaskUtils.toBitMaskValue(new Decimal128(new BigDecimal("11")))) );