Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ public class ExtraHeuristicEntryDto implements Serializable {

/**
* The type of extra heuristic.
* Note: for the moment, we only have heuristics on SQL, MONGO, OPENSEARCH, REDIS, DYNAMODB and CASSANDRA commands
* Note: for the moment, we only have heuristics on SQL, MONGO, OPENSEARCH, REDIS, DYNAMODB,
* CASSANDRA and NEO4J commands
*/
public enum Type {SQL, MONGO, OPENSEARCH, REDIS, DYNAMODB, CASSANDRA}
public enum Type {SQL, MONGO, OPENSEARCH, REDIS, DYNAMODB, CASSANDRA, NEO4J}

/**
* Should we try to minimize or maximize the heuristic?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,14 @@ default void extractRPCSchema(){}

default Object getMongoConnection() {return null;}

/**
* @return the Neo4j {@code org.neo4j.driver.Driver} of the SUT, or {@code null} if the SUT does
* not use Neo4j. Returned as {@code Object} and accessed by reflection, so the driver does not
* hard-depend on a specific {@code neo4j-java-driver} version. Used both to read the live graph
* when computing Cypher heuristics and (later) to insert test data.
*/
default Object getNeo4jConnection() {return null;}

default Object getOpenSearchConnection() {return null;}

default ReflectionBasedRedisClient getRedisConnection() {return null;}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@ public Response runSut(SutRunDto dto, @Context HttpServletRequest httpServletReq
noKillSwitch(() -> sutController.initSqlHandler());
noKillSwitch(() -> sutController.registerOrExecuteInitSqlCommandsIfNeeded(true));
noKillSwitch(() -> sutController.initMongoHandler());
noKillSwitch(() -> sutController.initNeo4jHandler());
noKillSwitch(() -> sutController.initOpenSearchHandler());
noKillSwitch(() -> sutController.initRedisHandler());
noKillSwitch(() -> sutController.initDynamoDbHandler());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.evomaster.client.java.sql.SqlScriptRunnerCached;
import org.evomaster.client.java.sql.DbSpecification;
import org.evomaster.client.java.controller.internal.db.mongo.MongoHandler;
import org.evomaster.client.java.controller.internal.db.neo4j.Neo4jHandler;
import org.evomaster.client.java.sql.DbInfoExtractor;
import org.evomaster.client.java.sql.internal.SqlHandler;
import org.evomaster.client.java.controller.mongo.MongoScriptRunner;
Expand Down Expand Up @@ -89,6 +90,8 @@ public abstract class SutController implements SutHandler, CustomizationHandler

private final MongoHandler mongoHandler = new MongoHandler();

private final Neo4jHandler neo4jHandler = new Neo4jHandler();

private final OpenSearchHandler openSearchHandler = new OpenSearchHandler();

private final RedisHandler redisHandler = new RedisHandler();
Expand Down Expand Up @@ -351,6 +354,10 @@ public final void initMongoHandler() {
}
}

public final void initNeo4jHandler() {
neo4jHandler.setNeo4jConnection(getNeo4jConnection());
}

// TODO: Refactor this initialization methods once Redis and OpenSearch implementations are done
public final void initOpenSearchHandler() {
// This is needed because the replacement use to get this info occurs during the start of the SUT.
Expand Down Expand Up @@ -397,6 +404,7 @@ public final boolean doEmploySmartDbClean(){
public final void resetExtraHeuristics() {
sqlHandler.reset();
mongoHandler.reset();
neo4jHandler.reset();
redisHandler.reset();
dynamoDbHandler.reset();
}
Expand All @@ -422,7 +430,7 @@ public final ExtraHeuristicsDto computeExtraHeuristics(boolean queryFromDatabase

if (isSQLHeuristicsComputationAllowed() || isMongoHeuristicsComputationAllowed()
|| isOpenSearchHeuristicsComputationAllowed() || isRedisHeuristicsComputationAllowed()
|| isDynamoDbHeuristicsComputationAllowed()) {
|| isDynamoDbHeuristicsComputationAllowed() || isNeo4jHeuristicsComputationAllowed()) {
List<AdditionalInfo> additionalInfoList = getAdditionalInfoList();

if (isSQLHeuristicsComputationAllowed()) {
Expand All @@ -431,6 +439,9 @@ public final ExtraHeuristicsDto computeExtraHeuristics(boolean queryFromDatabase
if (isMongoHeuristicsComputationAllowed()) {
computeMongoHeuristics(dto, additionalInfoList);
}
if (isNeo4jHeuristicsComputationAllowed()) {
computeNeo4jHeuristics(dto, additionalInfoList);
}
if (isOpenSearchHeuristicsComputationAllowed()) {
computeOpenSearchHeuristics(dto, additionalInfoList);
}
Expand All @@ -452,6 +463,10 @@ private boolean isMongoHeuristicsComputationAllowed() {
return mongoHandler.isCalculateHeuristics() || mongoHandler.isExtractMongoExecution();
}

private boolean isNeo4jHeuristicsComputationAllowed() {
return neo4jHandler.isCalculateHeuristics();
}

private boolean isOpenSearchHeuristicsComputationAllowed() {
return openSearchHandler.isCalculateHeuristics();
}
Expand Down Expand Up @@ -546,6 +561,34 @@ public final void computeMongoHeuristics(ExtraHeuristicsDto dto, List<Additional
}
}

public final void computeNeo4jHeuristics(ExtraHeuristicsDto dto, List<AdditionalInfo> additionalInfoList){
if(neo4jHandler.isCalculateHeuristics()){
if(!additionalInfoList.isEmpty()) {
AdditionalInfo last = additionalInfoList.get(additionalInfoList.size() - 1);
last.getNeo4JInfoData().forEach(it -> {
try {
neo4jHandler.handle(it);
} catch (Exception e){
SimpleLogger.error("FAILED TO HANDLE NEO4J COMMAND: " + e.getMessage());
assert false;
}
});
}

neo4jHandler.getEvaluatedNeo4jCommands().stream()
.map(p ->
new ExtraHeuristicEntryDto(
ExtraHeuristicEntryDto.Type.NEO4J,
ExtraHeuristicEntryDto.Objective.MINIMIZE_TO_ZERO,
p.getCommand(),
p.getDistanceWithMetrics().getDistance(),
p.getDistanceWithMetrics().getNumberOfEvaluatedNodes(),
p.getDistanceWithMetrics().isEvaluationFailure()
))
.forEach(h -> dto.heuristics.add(h));
}
}

public final void computeOpenSearchHeuristics(ExtraHeuristicsDto dto, List<AdditionalInfo> additionalInfoList) {
if (openSearchHandler.isCalculateHeuristics()) {
if (!additionalInfoList.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.evomaster.client.java.controller.internal.db.neo4j;

/**
* Pairs a captured Cypher query with its computed distance to being satisfied by the live graph.
*/
public final class Neo4jCommandWithDistance {

private final String command;
private final Neo4jDistanceWithMetrics distanceWithMetrics;

/**
* Creates the evaluation of one captured query.
*
* @param command the Cypher query, as executed by the SUT
* @param distanceWithMetrics its heuristic result
*/
public Neo4jCommandWithDistance(String command, Neo4jDistanceWithMetrics distanceWithMetrics) {
this.command = command;
this.distanceWithMetrics = distanceWithMetrics;
}

/**
* @return the Cypher query, as executed by the SUT
*/
public String getCommand() {
return command;
}

/**
* @return the heuristic result of the query
*/
public Neo4jDistanceWithMetrics getDistanceWithMetrics() {
return distanceWithMetrics;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package org.evomaster.client.java.controller.internal.db.neo4j;

import org.evomaster.client.java.controller.neo4j.heuristics.Neo4jHeuristicsCalculator;

/**
* Result of scoring one captured Cypher query against the live graph: the distance to satisfying the
* query, how many graph nodes were considered, and whether the evaluation failed.
* <p>
* The distance is {@code 1 - ofTrue} of the {@code Truthness} computed by the heuristics calculator, so a
* computed value lies in {@code [0,1]} by construction, with 0 meaning the query is satisfied. A failed
* evaluation carries {@link Neo4jHeuristicsCalculator#MAX_NEO4J_DISTANCE}.
*/
public final class Neo4jDistanceWithMetrics {

private final double distance;
private final int numberOfEvaluatedNodes;
private final boolean evaluationFailure;

/**
* Creates a Neo4j heuristic result.
*
* @param distance distance to satisfying the query, 0 meaning satisfied
* @param numberOfEvaluatedNodes number of graph nodes considered
* @param evaluationFailure whether the evaluation failed, in which case the distance must be
* {@link Neo4jHeuristicsCalculator#MAX_NEO4J_DISTANCE}
*/
public Neo4jDistanceWithMetrics(double distance, int numberOfEvaluatedNodes, boolean evaluationFailure) {
if (distance < 0.0d || Double.isNaN(distance)) {
throw new IllegalArgumentException("distance must be non-negative, but was " + distance);
}
if (numberOfEvaluatedNodes < 0) {
throw new IllegalArgumentException("numberOfEvaluatedNodes must be non-negative");
}
if (evaluationFailure && distance != Neo4jHeuristicsCalculator.MAX_NEO4J_DISTANCE) {
throw new IllegalArgumentException(
"a failed Neo4j distance computation cannot have a value different than MAX_NEO4J_DISTANCE");
}
this.distance = distance;
this.numberOfEvaluatedNodes = numberOfEvaluatedNodes;
this.evaluationFailure = evaluationFailure;
}

/**
* @return distance to satisfying the query, 0 meaning satisfied,
* {@link Neo4jHeuristicsCalculator#MAX_NEO4J_DISTANCE} on failure
*/
public double getDistance() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if normalized, then the name getDistance is confusing.

return distance;
}

/**
* @return number of graph nodes considered
*/
public int getNumberOfEvaluatedNodes() {
return numberOfEvaluatedNodes;
}

/**
* @return whether the evaluation failed
*/
public boolean isEvaluationFailure() {
return evaluationFailure;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package org.evomaster.client.java.controller.internal.db.neo4j;

import org.evomaster.client.java.controller.neo4j.data.Neo4jGraph;
import org.evomaster.client.java.controller.neo4j.heuristics.Neo4jHeuristicsCalculator;
import org.evomaster.client.java.controller.neo4j.operations.MatchOperation;
import org.evomaster.client.java.controller.neo4j.parser.CypherParser;
import org.evomaster.client.java.controller.neo4j.parser.CypherParserException;
import org.evomaster.client.java.controller.neo4j.parser.CypherParserFactory;
import org.evomaster.client.java.controller.internal.TaintHandlerExecutionTracer;
import org.evomaster.client.java.instrumentation.Neo4JRunCommand;
import org.evomaster.client.java.utils.SimpleLogger;

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

/**
* Acts upon Cypher queries executed by the SUT (captured as {@link Neo4JRunCommand}s): for each
* captured query it computes how close the live graph is to satisfying it, as a distance to minimize.
* Only MATCH queries are scored; a query that does not parse as a MATCH (e.g. a write) is skipped.
*/
public class Neo4jHandler {

/** Cypher queries captured from {@code Session.run}, pending evaluation. */
private final List<Neo4JRunCommand> operations;

/** The computed heuristics, one per scored query. */
private final List<Neo4jCommandWithDistance> commandsWithDistances;

/** Whether to compute heuristics based on execution or not. */
private volatile boolean calculateHeuristics;

/**
* The SUT's {@code org.neo4j.driver.Driver}, kept as an {@code Object} and used by reflection so
* we do not hard-depend on a specific driver version. {@code null} when the SUT does not use Neo4j.
*/
private Object neo4jConnection = null;

private final CypherParser parser = CypherParserFactory.buildParser();

private final Neo4jHeuristicsCalculator calculator =
new Neo4jHeuristicsCalculator(new TaintHandlerExecutionTracer());
private final Neo4jGraphReader graphReader = new Neo4jGraphReader();

/**
* Creates a handler with heuristic calculation enabled.
*/
public Neo4jHandler() {
operations = new ArrayList<>();
commandsWithDistances = new ArrayList<>();
calculateHeuristics = true;
}

/**
* Clears data collected for the current action.
*/
public void reset() {
operations.clear();
commandsWithDistances.clear();
}

/**
* @return whether Neo4j heuristic calculation is enabled
*/
public boolean isCalculateHeuristics() {
return calculateHeuristics;
}

/**
* Enables or disables Neo4j heuristic calculation.
*
* @param calculateHeuristics new calculation state
*/
public void setCalculateHeuristics(boolean calculateHeuristics) {
this.calculateHeuristics = calculateHeuristics;
}

/**
* Sets the driver used to read the live graph.
*
* @param neo4jConnection the SUT's {@code org.neo4j.driver.Driver}, or {@code null} if it has none
*/
public void setNeo4jConnection(Object neo4jConnection) {
this.neo4jConnection = neo4jConnection;
}

/**
* Registers one intercepted Cypher query.
*
* @param info intercepted query
*/
public void handle(Neo4JRunCommand info) {
if (calculateHeuristics && info.getQuery() != null) {
operations.add(info);
}
}

/**
* Evaluates all registered queries against a single snapshot of the graph, and consumes them.
* The snapshot is read once per action rather than per query, since the SUT is not running while
* the heuristics are computed.
*
* @return evaluated queries for the current action
*/
public List<Neo4jCommandWithDistance> getEvaluatedNeo4jCommands() {

if (!calculateHeuristics || neo4jConnection == null || operations.isEmpty()) {
operations.clear();
return commandsWithDistances;
}

Neo4jGraph graph;
try {
graph = graphReader.read(neo4jConnection);
} catch (Exception e) {
SimpleLogger.uniqueWarn("Failed to read the Neo4j graph to compute heuristics: " + e.getMessage());
operations.clear();
return commandsWithDistances;
}

for (Neo4JRunCommand op : operations) {
String query = op.getQuery();
final MatchOperation parsedQuery;
try {
parsedQuery = parser.parse(query);
} catch (CypherParserException e) {
SimpleLogger.uniqueWarn("Failed to parse Cypher query for Neo4j heuristics: " + e.getMessage());
continue;
}

Neo4jDistanceWithMetrics metrics;
try {
double distance = calculator.computeDistance(parsedQuery, graph);
metrics = new Neo4jDistanceWithMetrics(distance, graph.nodeCount(), false);
} catch (Exception e) {
SimpleLogger.uniqueWarn("Failed to compute Neo4j heuristic for query: " + query
+ " | cause: " + e.getClass().getName() + ": " + e.getMessage());
metrics = new Neo4jDistanceWithMetrics(Neo4jHeuristicsCalculator.MAX_NEO4J_DISTANCE, graph.nodeCount(), true);
}
commandsWithDistances.add(new Neo4jCommandWithDistance(query, metrics));
}

operations.clear();
return commandsWithDistances;
}
}
Loading