Skip to content
Open
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,9 @@ public class ExtraHeuristicEntryDto implements Serializable {

/**
* The type of extra heuristic.
* Note: for the moment, we only have heuristics on SQL, MONGO, OPENSEARCH and REDIS commands
* Note: for the moment, we only have heuristics on SQL, MONGO, OPENSEARCH, REDIS and NEO4J commands
*/
public enum Type {SQL, MONGO, OPENSEARCH, REDIS}
public enum Type {SQL, MONGO, OPENSEARCH, REDIS, 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());
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,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 @@ -88,6 +89,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 @@ -349,6 +352,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 @@ -388,6 +395,7 @@ public final boolean doEmploySmartDbClean(){
public final void resetExtraHeuristics() {
sqlHandler.reset();
mongoHandler.reset();
neo4jHandler.reset();
redisHandler.reset();
}

Expand All @@ -411,6 +419,7 @@ public final ExtraHeuristicsDto computeExtraHeuristics(boolean queryFromDatabase
ExtraHeuristicsDto dto = new ExtraHeuristicsDto();

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

Expand All @@ -420,6 +429,9 @@ public final ExtraHeuristicsDto computeExtraHeuristics(boolean queryFromDatabase
if (isMongoHeuristicsComputationAllowed()) {
computeMongoHeuristics(dto, additionalInfoList);
}
if (isNeo4jHeuristicsComputationAllowed()) {
computeNeo4jHeuristics(dto, additionalInfoList);
}
if (isOpenSearchHeuristicsComputationAllowed()) {
computeOpenSearchHeuristics(dto, additionalInfoList);
}
Expand All @@ -438,6 +450,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 @@ -528,6 +544,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");
assert false;
}
});
}

neo4jHandler.getEvaluatedCommands().stream()
.map(p ->
new ExtraHeuristicEntryDto(
ExtraHeuristicEntryDto.Type.NEO4J,
ExtraHeuristicEntryDto.Objective.MINIMIZE_TO_ZERO,
p.neo4jCommand,
p.neo4jDistanceWithMetrics.neo4jDistance,
p.neo4jDistanceWithMetrics.numberOfEvaluatedNodes,
p.neo4jDistanceWithMetrics.neo4jDistanceEvaluationFailure
))
.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,16 @@
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 class Neo4jCommandWithDistance {

public final String neo4jCommand;

public final Neo4jDistanceWithMetrics neo4jDistanceWithMetrics;

public Neo4jCommandWithDistance(String neo4jCommand, Neo4jDistanceWithMetrics neo4jDistanceWithMetrics) {
this.neo4jCommand = neo4jCommand;
this.neo4jDistanceWithMetrics = neo4jDistanceWithMetrics;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.evomaster.client.java.controller.internal.db.neo4j;

/**
* The result of scoring one captured Cypher query against the live graph: the distance to satisfying
* it ({@code 1 - ofTrue}, in {@code [0,1]}, 0 meaning satisfied), how many graph nodes were available
* when scoring, and whether the evaluation failed (e.g. the query could not be parsed).
*/
public class Neo4jDistanceWithMetrics {

public final double neo4jDistance;

public final int numberOfEvaluatedNodes;

public final boolean neo4jDistanceEvaluationFailure;

public Neo4jDistanceWithMetrics(double neo4jDistance, int numberOfEvaluatedNodes,
boolean neo4jDistanceEvaluationFailure) {
if (neo4jDistance < 0) {
throw new IllegalArgumentException("neo4jDistance must be non-negative but value is " + neo4jDistance);
}
if (numberOfEvaluatedNodes < 0) {
throw new IllegalArgumentException(
"numberOfEvaluatedNodes must be non-negative but value is " + numberOfEvaluatedNodes);
}
this.neo4jDistance = neo4jDistance;
this.numberOfEvaluatedNodes = numberOfEvaluatedNodes;
this.neo4jDistanceEvaluationFailure = neo4jDistanceEvaluationFailure;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
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.data.Neo4jNode;
import org.evomaster.client.java.controller.neo4j.data.Neo4jRelationship;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* Reads the live Neo4j graph into an in-memory {@link Neo4jGraph} snapshot, used as {@code G} when
* scoring Cypher queries. The whole graph is read with two read-only Cypher queries that return only
* primitive projections (ids, label/type names, property maps), so the reflection surface is just
* {@code Session.run} / {@code Result.list} / {@code Record.get} / {@code Value.as*} — never the
* driver's {@code Node}/{@code Relationship} types.
*/
public class Neo4jGraphReader {

private static final String NODE_QUERY =
"MATCH (n) RETURN elementId(n) AS id, labels(n) AS labels, properties(n) AS props";

private static final String REL_QUERY =
"MATCH ()-[r]->() RETURN elementId(r) AS id, type(r) AS type, "
+ "elementId(startNode(r)) AS src, elementId(endNode(r)) AS tgt, properties(r) AS props";

/**
* Reads all nodes and relationships from the database reachable through {@code driver}.
*
* @param driver the SUT's {@code org.neo4j.driver.Driver}, as an {@code Object}
* @return the in-memory graph snapshot
* @throws RuntimeException if the driver cannot be queried (wrapping the reflection failure)
*/
public Neo4jGraph read(Object driver) {
Object session = invoke(driver, "session");
try {
List<Neo4jNode> nodes = readNodes(session);
List<Neo4jRelationship> relationships = readRelationships(session);
return new Neo4jGraph(nodes, relationships);
} finally {
invoke(session, "close");
}
}

private List<Neo4jNode> readNodes(Object session) {
List<Neo4jNode> nodes = new ArrayList<>();
for (Object record : runAndList(session, NODE_QUERY)) {
String id = asString(get(record, "id"));
Set<String> labels = toStringSet(asList(get(record, "labels")));
Map<String, Object> props = asMap(get(record, "props"));
nodes.add(new Neo4jNode(id, labels, props));
}
return nodes;
}

private List<Neo4jRelationship> readRelationships(Object session) {
List<Neo4jRelationship> relationships = new ArrayList<>();
for (Object record : runAndList(session, REL_QUERY)) {
String id = asString(get(record, "id"));
String type = asString(get(record, "type"));
String src = asString(get(record, "src"));
String tgt = asString(get(record, "tgt"));
Map<String, Object> props = asMap(get(record, "props"));
relationships.add(new Neo4jRelationship(id, type, src, tgt, props));
}
return relationships;
}

private List<?> runAndList(Object session, String query) {
Object result = invoke(session, "run", new Class<?>[]{String.class}, query);
return (List<?>) invoke(result, "list");
}

private Object get(Object record, String key) {
return invoke(record, "get", new Class<?>[]{String.class}, key);
}

private String asString(Object value) {
return (String) invoke(value, "asString");
}

private List<?> asList(Object value) {
return (List<?>) invoke(value, "asList");
}

@SuppressWarnings("unchecked")
private Map<String, Object> asMap(Object value) {
return (Map<String, Object>) invoke(value, "asMap");
}

private Set<String> toStringSet(List<?> values) {
Set<String> set = new LinkedHashSet<>();
for (Object v : values) {
set.add(String.valueOf(v));
}
return set;
}

private Object invoke(Object target, String method) {
return invoke(target, method, new Class<?>[0]);
}

private Object invoke(Object target, String method, Class<?>[] argTypes, Object... args) {
try {
Method m = target.getClass().getMethod(method, argTypes);
m.setAccessible(true);
return m.invoke(target, args);
} catch (ReflectiveOperationException e) {
throw new RuntimeException("Failed to read the Neo4j graph via reflection (" + method + ")", e);
}
}
}
Loading
Loading