From 69c39edf82f903256cf498de82a82714e7c3b20a Mon Sep 17 00:00:00 2001 From: LautaroPetaccio Date: Tue, 18 Aug 2026 10:10:31 -0300 Subject: [PATCH] Fix $all heuristic to quantify over the expected values The truthness for {"f": {"$all": [...]}} was aggregated as an AND over the elements of the array held by "f", where each element was scored by an OR over the expected values. That asks "is every stored element one of the queried values", which is the inverse of $all: the operator requires every queried value to be present in the stored array, and is indifferent to extra elements. The direction was inverted in both ways: {tags: {$all: ["a"]}} + {tags: ["a", "b"]} matches in MongoDB, but was scored as false; {tags: {$all: ["a","b","c"]}} + {tags: ["a"]} does not match, but was scored as fully true, ie distance 0. The second case is the damaging one: a condition no data can satisfy is reported as already covered, so the search stops trying to reach it. Aggregate as an AND over the expected values instead, scoring each one with an OR over the elements of the array, which also restores a usable gradient: a document missing fewer of the expected values now scores closer to true. The existing tests only compared identical lists, where both directions agree, so they did not catch this. The three added tests cover each failure direction and the gradient. --- .../mongo/MongoHeuristicsCalculator.java | 34 +++++++++++++++++-- .../mongo/MongoHeuristicsCalculatorTest.java | 34 +++++++++++++++++++ 2 files changed, 65 insertions(+), 3 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..205c4c646e 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 @@ -532,6 +532,17 @@ private Truthness computeHeuristic(NotInOperation operation, Object document) } } + /** + * Computes the heuristic score for a {"f",{"$all": [v1, ..., vn] }} query. + * The condition holds when the array held by "f" contains every one of the expected values, + * so the score is an AND aggregation over the expected values, each of which is scored by an + * OR aggregation over the elements of the array. Note the direction: extra elements in the + * document are irrelevant, whereas a missing expected value makes the condition false. + * + * @param operation the {"f",{"$all": [...]}} query encapsulated as an AllOperation + * @param document the BSON document to evaluate the heuristic score against + * @return a Truthness object representing the distance of the document from meeting the condition + */ private Truthness computeHeuristic(AllOperation operation, Object document) { requireNonNullQueryAndDocument(operation, document); @@ -550,10 +561,10 @@ private Truthness computeHeuristic(AllOperation operation, Object document) { if (actualValuesList.isEmpty()) { return C_FALSE; } else { - Truthness res = buildAndAggregationTruthness(actualValuesList + Truthness res = buildAndAggregationTruthness(expectedValues .stream() - .map(actualValuesListElement -> - computeHeuristic(actualValuesListElement, expectedValues)) + .map(expectedValue -> + computeHeuristicForContainedValue(expectedValue, actualValuesList)) .toArray(Truthness[]::new)); return buildSafeScaledTruthness(res); } @@ -561,6 +572,23 @@ private Truthness computeHeuristic(AllOperation operation, Object document) { } } + /** + * Computes the heuristic score for the presence of a single expected value inside the array + * held by the queried field, ie an OR aggregation over the elements of that array. + * + * @param expectedValue a value that a {"f",{"$all": [...]}} query requires to be present + * @param actualValues the non-empty array held by the field "f" in the document + * @return a Truthness object representing how close the array is to containing the expected value + */ + private Truthness computeHeuristicForContainedValue(Object expectedValue, List actualValues) { + Objects.requireNonNull(actualValues); + + return buildOrAggregationTruthness(actualValues.stream() + .map(actualValue -> computeHeuristicComparisonNullableValues(expectedValue, actualValue, + SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO)) + .toArray(Truthness[]::new)); + } + private static Truthness buildSafeScaledTruthness(Truthness 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 50675c235e..b330d2c219 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 @@ -273,6 +273,40 @@ public void testAllBothActualAndExpectedListsAreEmpty() { assertTrue(calculator.computeHeuristicDocument(convertToDocument(allQuery), document).isFalse()); } + @Test + public void testAllMatchesWhenDocumentArrayHasExtraElements() { + // $all only requires the expected values to be present; extra elements are irrelevant + Document doc = new Document().append("employees", new ArrayList<>(Arrays.asList(1, 5, 6))); + Bson allQuery = Filters.all("employees", new ArrayList<>(Arrays.asList(1, 5))); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + assertTrue(calculator.computeHeuristicDocument(convertToDocument(allQuery), doc).isTrue()); + } + + @Test + public void testAllDoesNotMatchWhenAnExpectedValueIsMissing() { + // the array contains 1, but not 2 nor 3, so the condition does not hold + Document doc = new Document().append("employees", new ArrayList<>(Arrays.asList(1))); + Bson allQuery = Filters.all("employees", new ArrayList<>(Arrays.asList(1, 2, 3))); + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + assertTrue(calculator.computeHeuristicDocument(convertToDocument(allQuery), doc).isFalse()); + } + + @Test + public void testAllGivesBetterScoreWhenFewerExpectedValuesAreMissing() { + Bson allQuery = Filters.all("employees", new ArrayList<>(Arrays.asList(1, 2, 3))); + Document closer = new Document().append("employees", new ArrayList<>(Arrays.asList(1, 2))); + Document farther = new Document().append("employees", new ArrayList<>(Arrays.asList(1))); + + MongoHeuristicsCalculator calculator = new MongoHeuristicsCalculator(); + Truthness closerTruthness = calculator.computeHeuristicDocument(convertToDocument(allQuery), closer); + Truthness fartherTruthness = calculator.computeHeuristicDocument(convertToDocument(allQuery), farther); + + assertTrue(closerTruthness.isFalse()); + assertTrue(fartherTruthness.isFalse()); + assertTrue(closerTruthness.getOfTrue() > fartherTruthness.getOfTrue(), + "a document missing fewer expected values must be scored closer to true"); + } + @Test public void testSize() {