diff --git a/client-java/controller-api/pom.xml b/client-java/controller-api/pom.xml index c7fab551a0..3120ab2676 100644 --- a/client-java/controller-api/pom.xml +++ b/client-java/controller-api/pom.xml @@ -22,6 +22,10 @@ org.evomaster evomaster-client-java-sql-dto + + org.evomaster + evomaster-client-java-instrumentation-shared + org.evomaster evomaster-test-utils-java diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/SutRunDto.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/SutRunDto.java index 95f8149f45..e2039bc8f3 100644 --- a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/SutRunDto.java +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/SutRunDto.java @@ -45,6 +45,16 @@ public class SutRunDto { */ public String methodReplacementCategories; + /** + * Whether to enable Dynamosa graphs generation in the Java agent + */ + public Boolean enableDynamosaGraphs; + + /** + * Whether to write generated graphs (DOT/PNGs) to disk on the agent side + */ + public Boolean writeCfg; + public SutRunDto() { } diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/TestResultsDto.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/TestResultsDto.java index 5f34446961..8f8a6a1efb 100644 --- a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/TestResultsDto.java +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/TestResultsDto.java @@ -1,5 +1,7 @@ package org.evomaster.client.java.controller.api.dto; +import org.evomaster.client.java.instrumentation.shared.dto.ControlDependenceGraphDto; + import java.util.ArrayList; import java.util.List; @@ -14,4 +16,9 @@ public class TestResultsDto { public List additionalInfoList = new ArrayList<>(); public List extraHeuristics = new ArrayList<>(); + + /** + * Incremental DynaMOSA control-dependence graphs discovered since the last handshake. + */ + public List dynamosaCdgs = new ArrayList<>(); } diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/EmbeddedSutController.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/EmbeddedSutController.java index 9b8128dc49..22d3606268 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/EmbeddedSutController.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/EmbeddedSutController.java @@ -6,6 +6,8 @@ import org.evomaster.client.java.controller.api.dto.problem.rpc.ScheduleTaskInvocationDto; import org.evomaster.client.java.controller.internal.SutController; import org.evomaster.client.java.instrumentation.*; +import org.evomaster.client.java.instrumentation.external.DynamosaControlDependenceSnapshot; +import org.evomaster.client.java.instrumentation.shared.dto.ControlDependenceGraphDto; import org.evomaster.client.java.instrumentation.object.ClassToSchema; import org.evomaster.client.java.instrumentation.staticstate.ExecutionTracer; import org.evomaster.client.java.instrumentation.staticstate.ObjectiveRecorder; @@ -31,6 +33,8 @@ */ public abstract class EmbeddedSutController extends SutController { + private int dynamosaCdgIndex = 0; + @Override public final void setupForGeneratedTest(){ //In the past, we configured P6Spy here @@ -138,4 +142,21 @@ public final void getJvmDtoSchema(List dtoNames) { public final void bootingSut(boolean bootingSut) { ObjectiveRecorder.setBooting(bootingSut); } + + @Override + public List getDynamosaControlDependenceGraphs() { + DynamosaControlDependenceSnapshot snapshot = InstrumentationController.getControlDependenceSnapshot(dynamosaCdgIndex); + dynamosaCdgIndex = snapshot.getNextIndex(); + return snapshot.getGraphs(); + } + + @Override + public void setDynamosaGraphsEnabled(boolean enableGraphs) { + InstrumentationController.setDynamosaGraphsEnabled(enableGraphs); + } + + @Override + public void setWriteCfgEnabled(boolean writeCfg) { + InstrumentationController.setWriteCfgEnabled(writeCfg); + } } diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/ExternalSutController.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/ExternalSutController.java index 08c08b5814..e077b78222 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/ExternalSutController.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/ExternalSutController.java @@ -12,6 +12,8 @@ import org.evomaster.client.java.controller.internal.SutController; import org.evomaster.client.java.instrumentation.external.JarAgentLocator; import org.evomaster.client.java.instrumentation.external.ServerController; +import org.evomaster.client.java.instrumentation.external.DynamosaConfigDto; +import org.evomaster.client.java.instrumentation.shared.dto.ControlDependenceGraphDto; import java.io.BufferedReader; import java.io.IOException; @@ -447,6 +449,15 @@ public BootTimeInfoDto getBootTimeInfoDto() { return getBootTimeInfoDto(serverController.handleBootTimeObjectiveInfo()); } + @Override + public List getDynamosaControlDependenceGraphs() { + if (!isInstrumentationActivated()){ + return Collections.emptyList(); + } + List graphs = serverController.getDynamosaControlDependenceGraphs(); + return graphs != null ? graphs : Collections.emptyList(); + } + @Override public final void newActionSpecificHandler(ActionDto dto) { if (isInstrumentationActivated()) { @@ -522,6 +533,28 @@ public final void setExecutingInitMongo(boolean executingInitMongo) { ExecutionTracer.setExecutingInitMongo(executingInitMongo); } + /** + * Send Dynamosa configuration to the Java Agent. + */ + public final void setDynamosaGraphsEnabled(boolean enableGraphs) { + checkInstrumentation(); + DynamosaConfigDto dto = new DynamosaConfigDto(); + dto.enableGraphs = enableGraphs; + dto.writeCfg = null; + serverController.setDynamosaConfig(dto); + } + + /** + * Control whether the agent writes DOT/PNG graphs to disk. + */ + public final void setWriteCfgEnabled(boolean writeCfg) { + checkInstrumentation(); + DynamosaConfigDto dto = new DynamosaConfigDto(); + dto.enableGraphs = null; + dto.writeCfg = writeCfg; + serverController.setDynamosaConfig(dto); + } + @Override public final void setExecutingAction(boolean executingAction){ checkInstrumentation(); diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/EMController.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/EMController.java index ea08188418..6a474f5f75 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/EMController.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/EMController.java @@ -19,6 +19,7 @@ import org.evomaster.client.java.sql.SqlScriptRunner; import org.evomaster.client.java.controller.problem.rpc.schema.LocalAuthSetupSchema; import org.evomaster.client.java.instrumentation.*; +import org.evomaster.client.java.instrumentation.shared.dto.ControlDependenceGraphDto; import org.evomaster.client.java.instrumentation.shared.StringSpecializationInfo; import org.evomaster.client.java.instrumentation.staticstate.ExecutionTracer; import org.evomaster.client.java.utils.SimpleLogger; @@ -254,7 +255,6 @@ public Response getSutInfo(@Context HttpServletRequest httpServletRequest) { return Response.status(500).entity(WrappedResponseDto.withError(msg)).build(); } - return Response.status(200).entity(WrappedResponseDto.withData(dto)).build(); } @@ -377,6 +377,11 @@ public Response runSut(SutRunDto dto, @Context HttpServletRequest httpServletReq if (!noKillSwitch(() -> sutController.isSutRunning())) { noKillSwitch(() -> sutController.bootingSut(true)); baseUrlOfSUT = noKillSwitch(() -> sutController.startSut()); + // Configure Dynamosa graphs on the agent, if requested by core + Boolean enableGraphs = dto.enableDynamosaGraphs; + if (enableGraphs != null) noKillSwitch(() -> sutController.setDynamosaGraphsEnabled(enableGraphs)); + Boolean writeCfg = dto.writeCfg; + if (writeCfg != null) noKillSwitch(() -> sutController.setWriteCfgEnabled(writeCfg)); noKillSwitch(() -> sutController.bootingSut(false)); if (baseUrlOfSUT == null) { //there has been an internal failure in starting the SUT @@ -588,6 +593,12 @@ public Response getTestResults( SimpleLogger.error(msg); return Response.status(500).entity(WrappedResponseDto.withError(msg)).build(); } + + List dynamosaCdgs = + noKillSwitch(() -> sutController.getDynamosaControlDependenceGraphs()); + if (dynamosaCdgs != null && !dynamosaCdgs.isEmpty()) { + dto.dynamosaCdgs.addAll(dynamosaCdgs); + } // } // else { // // there is still some data that we need during minimization diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/SutController.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/SutController.java index e6f64b89bf..bfa81cb1e7 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/SutController.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/SutController.java @@ -49,6 +49,7 @@ import org.evomaster.client.java.instrumentation.AdditionalInfo; import org.evomaster.client.java.instrumentation.BootTimeObjectiveInfo; import org.evomaster.client.java.instrumentation.TargetInfo; +import org.evomaster.client.java.instrumentation.shared.dto.ControlDependenceGraphDto; import org.evomaster.client.java.instrumentation.staticstate.UnitsInfoRecorder; import org.evomaster.client.java.utils.SimpleLogger; import org.glassfish.jersey.jackson.JacksonFeature; @@ -1564,6 +1565,22 @@ public abstract List getTargetInfos(Collection ids, public abstract void setExecutingAction(boolean executingAction); + /** + * Enable/disable Dynamosa graphs generation in the Java agent. + * Default no-op; external controllers can override to send to agent. + */ + public void setDynamosaGraphsEnabled(boolean enableGraphs){ + // no-op by default + } + + /** + * Enable/disable writing graphs to disk in the Java agent. + * Default no-op; external controllers can override to send to agent. + */ + public void setWriteCfgEnabled(boolean writeCfg){ + // no-op by default + } + /** * specify whether the SUT is booting (ie starting up), or not. @@ -1576,6 +1593,10 @@ public abstract List getTargetInfos(Collection ids, public abstract BootTimeInfoDto getBootTimeInfoDto(); + public List getDynamosaControlDependenceGraphs(){ + return Collections.emptyList(); + } + protected BootTimeInfoDto getBootTimeInfoDto(BootTimeObjectiveInfo info){ if (info == null) return null; diff --git a/client-java/instrumentation-shared/src/main/java/org/evomaster/client/java/instrumentation/shared/dto/ControlDependenceGraphDto.java b/client-java/instrumentation-shared/src/main/java/org/evomaster/client/java/instrumentation/shared/dto/ControlDependenceGraphDto.java new file mode 100644 index 0000000000..2e14e59966 --- /dev/null +++ b/client-java/instrumentation-shared/src/main/java/org/evomaster/client/java/instrumentation/shared/dto/ControlDependenceGraphDto.java @@ -0,0 +1,200 @@ +package org.evomaster.client.java.instrumentation.shared.dto; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Serializable representation of the control-dependence information of a method. + * Contains only the data needed by EvoMaster core: branch objective identifiers, + * the subset that are roots, and the parent→child relationships between them. + */ +public class ControlDependenceGraphDto implements Serializable { + + private static final long serialVersionUID = -1703430508792441369L; + + private String className; + private String methodName; + private List objectives = new ArrayList<>(); + private List rootObjectiveIds = new ArrayList<>(); + private List edges = new ArrayList<>(); + + public ControlDependenceGraphDto() { + } + + public ControlDependenceGraphDto(String className, + String methodName, + List objectives, + List rootObjectiveIds, + List edges) { + this.className = className; + this.methodName = methodName; + if (objectives != null) { + this.objectives = new ArrayList<>(objectives); + } + if (rootObjectiveIds != null) { + this.rootObjectiveIds = new ArrayList<>(rootObjectiveIds); + } + if (edges != null) { + this.edges = new ArrayList<>(edges); + } + } + + public String getClassName() { + return className; + } + + public void setClassName(String className) { + this.className = className; + } + + public String getMethodName() { + return methodName; + } + + public void setMethodName(String methodName) { + this.methodName = methodName; + } + + public List getObjectives() { + return Collections.unmodifiableList(objectives); + } + + public void setObjectives(List objectives) { + this.objectives = objectives == null ? new ArrayList<>() : new ArrayList<>(objectives); + } + + public List getRootObjectiveIds() { + return Collections.unmodifiableList(rootObjectiveIds); + } + + public void setRootObjectiveIds(List rootObjectiveIds) { + this.rootObjectiveIds = rootObjectiveIds == null ? new ArrayList<>() : new ArrayList<>(rootObjectiveIds); + } + + public List getEdges() { + return Collections.unmodifiableList(edges); + } + + public void setEdges(List edges) { + this.edges = edges == null ? new ArrayList<>() : new ArrayList<>(edges); + } + + public void addObjective(BranchObjectiveDto objective) { + if (objective != null) { + this.objectives.add(objective); + } + } + + public void addRootObjectiveId(Integer id) { + if (id != null) { + this.rootObjectiveIds.add(id); + } + } + + public void addEdge(DependencyEdgeDto edge) { + if (edge != null) { + this.edges.add(edge); + } + } + + /** + * Descriptor and numeric id for a branch objective (true or false outcome). + */ + public static class BranchObjectiveDto implements Serializable { + + private static final long serialVersionUID = -8122197698885173402L; + + private int id; + private String descriptiveId; + + public BranchObjectiveDto() { + } + + public BranchObjectiveDto(int id, String descriptiveId) { + this.id = id; + this.descriptiveId = descriptiveId; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getDescriptiveId() { + return descriptiveId; + } + + public void setDescriptiveId(String descriptiveId) { + this.descriptiveId = descriptiveId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BranchObjectiveDto that = (BranchObjectiveDto) o; + return id == that.id && Objects.equals(descriptiveId, that.descriptiveId); + } + + @Override + public int hashCode() { + return Objects.hash(id, descriptiveId); + } + } + + /** + * Directed edge describing that {@code parentObjectiveId} must be covered before {@code childObjectiveId}. + */ + public static class DependencyEdgeDto implements Serializable { + + private static final long serialVersionUID = 3167520314102399629L; + + private int parentObjectiveId; + private int childObjectiveId; + + public DependencyEdgeDto() { + } + + public DependencyEdgeDto(int parentObjectiveId, int childObjectiveId) { + this.parentObjectiveId = parentObjectiveId; + this.childObjectiveId = childObjectiveId; + } + + public int getParentObjectiveId() { + return parentObjectiveId; + } + + public void setParentObjectiveId(int parentObjectiveId) { + this.parentObjectiveId = parentObjectiveId; + } + + public int getChildObjectiveId() { + return childObjectiveId; + } + + public void setChildObjectiveId(int childObjectiveId) { + this.childObjectiveId = childObjectiveId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DependencyEdgeDto that = (DependencyEdgeDto) o; + return parentObjectiveId == that.parentObjectiveId && + childObjectiveId == that.childObjectiveId; + } + + @Override + public int hashCode() { + return Objects.hash(parentObjectiveId, childObjectiveId); + } + } +} + diff --git a/client-java/instrumentation/pom.xml b/client-java/instrumentation/pom.xml index cf5284c16e..b4b9dc84d0 100644 --- a/client-java/instrumentation/pom.xml +++ b/client-java/instrumentation/pom.xml @@ -181,7 +181,6 @@ com.google.guava guava - test org.mongodb diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/InstrumentationController.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/InstrumentationController.java index 26ba74454e..ccaafe3da5 100644 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/InstrumentationController.java +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/InstrumentationController.java @@ -1,9 +1,11 @@ package org.evomaster.client.java.instrumentation; -import org.evomaster.client.java.instrumentation.object.ClassToSchema; +import org.evomaster.client.java.instrumentation.dynamosa.graphs.GraphPool; import org.evomaster.client.java.instrumentation.staticstate.ExecutionTracer; import org.evomaster.client.java.instrumentation.staticstate.ObjectiveRecorder; import org.evomaster.client.java.instrumentation.staticstate.UnitsInfoRecorder; +import org.evomaster.client.java.instrumentation.dynamosa.DynamosaConfig; +import org.evomaster.client.java.instrumentation.external.DynamosaControlDependenceSnapshot; import java.util.ArrayList; import java.util.Collection; @@ -16,6 +18,7 @@ public class InstrumentationController { public static void resetForNewSearch(){ ExecutionTracer.reset(); ObjectiveRecorder.reset(false); + GraphPool.refreshAllCdgs(); } /* @@ -146,4 +149,16 @@ public static void extractSpecifiedDto(List dtoNames){ UnitsInfoRecorder.registerSpecifiedDtoSchema(ExtractJvmClass.extractAsSchema(dtoNames)); } + public static DynamosaControlDependenceSnapshot getControlDependenceSnapshot(int fromIndex){ + return GraphPool.exportSnapshotFromIndex(fromIndex); + } + + public static void setDynamosaGraphsEnabled(boolean enableGraphs) { + DynamosaConfig.setEnableGraphs(enableGraphs); + } + + public static void setWriteCfgEnabled(boolean writeCfg) { + DynamosaConfig.setWriteCfgEnabled(writeCfg); + } + } diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/Instrumentator.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/Instrumentator.java index 2853537d93..889d75bf02 100644 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/Instrumentator.java +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/Instrumentator.java @@ -7,6 +7,8 @@ import org.evomaster.client.java.instrumentation.shared.ClassName; import org.evomaster.client.java.instrumentation.staticstate.UnitsInfoRecorder; import org.evomaster.client.java.utils.SimpleLogger; +import org.evomaster.client.java.instrumentation.dynamosa.DynamosaConfig; +import org.evomaster.client.java.instrumentation.dynamosa.visitor.CFGClassVisitor; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; @@ -65,11 +67,15 @@ public byte[] transformBytes(ClassLoader classLoader, ClassName className, Class ClassNode cn = new ClassNode(); reader.accept(cn, readFlags); - if(canInstrumentForCoverage(className)){ + boolean canCollectCoverage = canInstrumentForCoverage(className); + if(canCollectCoverage){ + if (DynamosaConfig.isGraphsEnabled()){ + cv = new CFGClassVisitor(classLoader, cv); + } cv = new CoverageClassVisitor(cv, className); } else { cv = new ThirdPartyClassVisitor(cv, className); - } + } try { cn.accept(cv); diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/AnnotatedLabel.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/AnnotatedLabel.java new file mode 100755 index 0000000000..fdfa1cc9cd --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/AnnotatedLabel.java @@ -0,0 +1,57 @@ +/* + * Adapted from the EvoSuite project (https://github.com/EvoSuite/evosuite) + * and modified for use in EvoMaster's Dynamosa module. + */ +package org.evomaster.client.java.instrumentation.dynamosa; + +import org.objectweb.asm.Label; +import org.objectweb.asm.tree.LabelNode; + +/** + * Annotated labels are used to identify instrumented code + */ +public class AnnotatedLabel extends Label { + + private boolean isStart = false; + + private boolean ignore = false; + + private boolean ignoreFalse = false; + + private LabelNode parent = null; + + public AnnotatedLabel(boolean ignore, boolean start) { + this.ignore = ignore; + this.isStart = start; + } + + public AnnotatedLabel(boolean ignore, boolean start, LabelNode parent) { + this.ignore = ignore; + this.isStart = start; + this.parent = parent; + } + + public boolean isStartTag() { + return isStart; + } + + public boolean shouldIgnore() { + return ignore; + } + + public void setIgnoreFalse(boolean value) { + ignoreFalse = value; + } + + public boolean shouldIgnoreFalse() { + return ignoreFalse; + } + + public LabelNode getParent() { + return parent; + } + + public void setParent(LabelNode parent) { + this.parent = parent; + } +} diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/AnnotatedMethodNode.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/AnnotatedMethodNode.java new file mode 100755 index 0000000000..e04dde9f3d --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/AnnotatedMethodNode.java @@ -0,0 +1,52 @@ +/* + * Adapted from the EvoSuite project (https://github.com/EvoSuite/evosuite) + * and modified for use in EvoMaster's Dynamosa module. + */ +package org.evomaster.client.java.instrumentation.dynamosa; + +import org.objectweb.asm.Label; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.MethodNode; + +/** + * AnnotatedMethodNode wraps ASM's MethodNode to support Dynamosa-specific metadata. + */ +public class AnnotatedMethodNode extends MethodNode { + + /** + *

Constructor for AnnotatedMethodNode.

+ * + * @param access a int. + * @param name a {@link java.lang.String} object. + * @param desc a {@link java.lang.String} object. + * @param signature a {@link java.lang.String} object. + * @param exceptions an array of {@link java.lang.String} objects. + */ + public AnnotatedMethodNode(int access, String name, String desc, String signature, + String[] exceptions) { + super(Opcodes.ASM9, access, name, desc, signature, exceptions); + } + + /** + * {@inheritDoc} + *

+ * Returns the LabelNode corresponding to the given Label. Creates a new + * LabelNode if necessary. The default implementation of this method uses + * the {@link Label#info} field to store associations between labels and + * label nodes. + */ + @Override + protected LabelNode getLabelNode(final Label l) { + if (l instanceof AnnotatedLabel) { + AnnotatedLabel al = (AnnotatedLabel) l; + al.setParent(new LabelNode(al)); + return al.getParent(); + } else { + if (!(l.info instanceof LabelNode)) { + l.info = new LabelNode(l); + } + return (LabelNode) l.info; + } + } +} diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/DynamosaConfig.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/DynamosaConfig.java new file mode 100644 index 0000000000..1010449e9c --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/DynamosaConfig.java @@ -0,0 +1,44 @@ +/* + * Adapted from the EvoSuite project (https://github.com/EvoSuite/evosuite) + * and modified for use in EvoMaster's Dynamosa module. + */ +package org.evomaster.client.java.instrumentation.dynamosa; + +/** + * Runtime configuration for DYNAMOSA-related instrumentation features. + * Populated via the agent control channel. + */ +public class DynamosaConfig { + + private static volatile boolean enableGraphs = false; + private static volatile boolean writeCfg = false; + + + public static boolean isGraphsEnabled() { + System.out.println("enableGraphs: " + enableGraphs); + return enableGraphs; + } + + public static void setEnableGraphs(boolean value) { + System.out.println("----------------------------------------------"); + System.out.println("Setting enableGraphs to " + value); + System.out.println("----------------------------------------------"); + enableGraphs = value; + } + + public static boolean isWriteCfgEnabled() { + System.out.println("----------------------------------------------"); + System.out.println("writeCfg: " + writeCfg); + System.out.println("----------------------------------------------"); + return writeCfg; + } + + public static void setWriteCfgEnabled(boolean value) { + System.out.println("----------------------------------------------"); + System.out.println("Setting writeCfg to " + value); + System.out.println("----------------------------------------------"); + writeCfg = value; + } +} + + diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/EvoMasterGraph.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/EvoMasterGraph.java new file mode 100755 index 0000000000..92be370ade --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/EvoMasterGraph.java @@ -0,0 +1,815 @@ +/* + * Adapted from the EvoSuite project (https://github.com/EvoSuite/evosuite) + * and modified for use in EvoMaster's Dynamosa module. + */ +package org.evomaster.client.java.instrumentation.dynamosa.graphs; + +import org.jgrapht.DirectedGraph; +import org.jgrapht.alg.DijkstraShortestPath; +import org.jgrapht.ext.*; +import org.jgrapht.graph.DefaultDirectedGraph; +import org.jgrapht.graph.DefaultEdge; +import org.evomaster.client.java.utils.SimpleLogger; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.*; + +import static java.util.stream.Collectors.toCollection; + +public abstract class EvoMasterGraph { + + private static int evoSuiteGraphs = 0; + protected int graphId; + + protected DirectedGraph graph; + protected Class edgeClass; + + ComponentAttributeProvider vertexAttributeProvider = null; + ComponentAttributeProvider edgeAttributeProvider = null; + + protected EvoMasterGraph(Class edgeClass) { + + graph = new DefaultDirectedGraph<>(edgeClass); + this.edgeClass = edgeClass; + + setId(); + } + + protected EvoMasterGraph(DirectedGraph graph, Class edgeClass) { + if (graph == null || edgeClass == null) + throw new IllegalArgumentException("null given"); + + this.graph = graph; + this.edgeClass = edgeClass; + + setId(); + } + + private void setId() { + evoSuiteGraphs++; + graphId = evoSuiteGraphs; + } + + // retrieving nodes and edges + + /** + *

getEdgeSource

+ * + * @param e a E object. + * @return a V object. + */ + public V getEdgeSource(E e) { + if (!containsEdge(e)) + throw new IllegalArgumentException("edge not in graph"); + + return graph.getEdgeSource(e); + } + + /** + *

getEdgeTarget

+ * + * @param e a E object. + * @return a V object. + */ + public V getEdgeTarget(E e) { + if (!containsEdge(e)) + throw new IllegalArgumentException("edge not in graph"); + + return graph.getEdgeTarget(e); + } + + /** + *

outgoingEdgesOf

+ * + * @param node a V object. + * @return a {@link java.util.Set} object. + */ + public Set outgoingEdgesOf(V node) { + if (!containsVertex(node)) // should this just return null? + throw new IllegalArgumentException( + "node not contained in this graph"); + // TODO hash set? can't be sure E implements hash correctly + return new LinkedHashSet<>(graph.outgoingEdgesOf(node)); + } + + /** + *

incomingEdgesOf

+ * + * @param node a V object. + * @return a {@link java.util.Set} object. + */ + public Set incomingEdgesOf(V node) { + if (!containsVertex(node)) // should this just return null? + throw new IllegalArgumentException("node not contained in this graph "); + // TODO hash set? can't be sure E implements hash correctly + return new LinkedHashSet<>(graph.incomingEdgesOf(node)); + } + + /** + *

getChildren

+ * + * @param node a V object. + * @return a {@link java.util.Set} object. + */ + public Set getChildren(V node) { + if (!containsVertex(node)) { + SimpleLogger.warn("getChildren call requests a node not contained in the current graph. Node: " + node); + return null; + } + //TODO check why in project 57_hft-bomberman class client.gui.StartFrame this happens + // throw new IllegalArgumentException( + // "node not contained in this graph"); + // TODO hash set? can't be sure V implements hash correctly + Set r = outgoingEdgesOf(node).stream() + .map(this::getEdgeTarget) + .collect(toCollection(LinkedHashSet::new)); + + // sanity check + if (r.size() != outDegreeOf(node)) + throw new IllegalStateException( + "expect children count and size of set of all children of a graphs node to be equal"); + + return r; + } + + /** + *

getParents

+ * + * @param node a V object. + * @return a {@link java.util.Set} object. + */ + public Set getParents(V node) { + if (!containsVertex(node)) // should this just return null? + throw new IllegalArgumentException( + "node not contained in this graph"); + // TODO hash set? can't be sure V implements hash correctly + Set r = incomingEdgesOf(node).stream() + .map(this::getEdgeSource) + .collect(toCollection(LinkedHashSet::new)); + + // sanity check + if (r.size() != inDegreeOf(node)) + throw new IllegalStateException( + "expect parent count and size of set of all parents of a graphs node to be equal"); + + return r; + } + + /** + *

vertexSet

+ * + * @return a {@link java.util.Set} object. + */ + public Set vertexSet() { + // TODO hash set? can't be sure V implements hash correctly + return new LinkedHashSet<>(graph.vertexSet()); + /* + * Set r = new HashSet(); + * + * for (V v : graph.vertexSet()) r.add(v); + * + * return r; + */ + } + + /** + *

edgeSet

+ * + * @return a {@link java.util.Set} object. + */ + public Set edgeSet() { + // TODO hash set? can't be sure E implements hash correctly + return new LinkedHashSet<>(graph.edgeSet()); + + /* + * Set r = new HashSet(); + * + * for (E e : graph.edgeSet()) r.add(e); + * + * return r; + */ + } + + /** + * If the given node is contained within this graph and has exactly one + * child v this method will return v. Otherwise it will return null + * + * @param node a V object. + * @return a V object. + */ + public V getSingleChild(V node) { + if (node == null) + return null; + if (!graph.containsVertex(node)) + return null; + if (outDegreeOf(node) != 1) + return null; + + for (V r : getChildren(node)) + return r; + // should be unreachable + return null; + } + + // building the graph + + protected void addVertices(EvoMasterGraph other) { + + addVertices(other.vertexSet()); + } + + /** + *

addVertices

+ * + * @param vs a {@link java.util.Collection} object. + */ + protected void addVertices(Collection vs) { + if (vs == null) + throw new IllegalArgumentException("null given"); + for (V v : vs) + if (!addVertex(v)) + throw new IllegalArgumentException( + "unable to add all nodes in given collection: " + + v.toString()); + + } + + /** + *

addVertex

+ * + * @param v a V object. + * @return a boolean. + */ + protected boolean addVertex(V v) { + return graph.addVertex(v); + } + + /** + *

addEdge

+ * + * @param src a V object. + * @param target a V object. + * @return a E object. + */ + protected E addEdge(V src, V target) { + + return graph.addEdge(src, target); + } + + /** + *

addEdge

+ * + * @param src a V object. + * @param target a V object. + * @param e a E object. + * @return a boolean. + */ + protected boolean addEdge(V src, V target, E e) { + + return graph.addEdge(src, target, e); + } + + /** + * Redirects all edges going into node from to the node newStart and all + * edges going out of node from to the node newEnd. + *

+ * All three edges have to be present in the graph prior to a call to this + * method. + * + * @param from a V object. + * @param newStart a V object. + * @param newEnd a V object. + * @return a boolean. + */ + protected boolean redirectEdges(V from, V newStart, V newEnd) { + if (!(containsVertex(from) && containsVertex(newStart) && containsVertex(newEnd))) + throw new IllegalArgumentException( + "expect all given nodes to be present in this graph"); + + if (!redirectIncomingEdges(from, newStart)) + return false; + + return redirectOutgoingEdges(from, newEnd); + + } + + /** + * Redirects all incoming edges to oldNode to node newNode by calling + * redirectEdgeTarget for each incoming edge of oldNode + * + * @param oldNode a V object. + * @param newNode a V object. + * @return a boolean. + */ + protected boolean redirectIncomingEdges(V oldNode, V newNode) { + return incomingEdgesOf(oldNode).stream() + .allMatch(incomingEdge -> redirectEdgeTarget(incomingEdge, newNode)); + } + + /** + * Redirects all outgoing edges to oldNode to node newNode by calling + * redirectEdgeSource for each outgoing edge of oldNode + * + * @param oldNode a V object. + * @param newNode a V object. + * @return a boolean. + */ + protected boolean redirectOutgoingEdges(V oldNode, V newNode) { + return outgoingEdgesOf(oldNode).stream() + .allMatch(outgoingEdge -> redirectEdgeSource(outgoingEdge, newNode)); + } + + /** + * Redirects the edge target of the given edge to the given node by removing + * the given edge from the graph and reinserting it from the original source + * node to the given node + * + * @param edge a E object. + * @param node a V object. + * @return a boolean. + */ + protected boolean redirectEdgeTarget(E edge, V node) { + if (!(containsVertex(node) && containsEdge(edge))) + throw new IllegalArgumentException( + "edge and node must be present in this graph"); + + V edgeSource = graph.getEdgeSource(edge); + if (!graph.removeEdge(edge)) + return false; + + return addEdge(edgeSource, node, edge); + } + + /** + * Redirects the edge source of the given edge to the given node by removing + * the given edge from the graph and reinserting it from the given node to + * the original target node + * + * @param edge a E object. + * @param node a V object. + * @return a boolean. + */ + protected boolean redirectEdgeSource(E edge, V node) { + if (!(containsVertex(node) && containsEdge(edge))) + throw new IllegalArgumentException( + "edge and node must be present in this graph"); + + V edgeTarget = graph.getEdgeTarget(edge); + if (!graph.removeEdge(edge)) + return false; + + return addEdge(node, edgeTarget, edge); + } + + // different counts + + /** + *

vertexCount

+ * + * @return a int. + */ + public int vertexCount() { + return graph.vertexSet().size(); + } + + /** + *

edgeCount

+ * + * @return a int. + */ + public int edgeCount() { + return graph.edgeSet().size(); + } + + /** + *

outDegreeOf

+ * + * @param node a V object. + * @return a int. + */ + public int outDegreeOf(V node) { // TODO rename to sth. like childCount() + if (node == null || !containsVertex(node)) + return -1; + + return graph.outDegreeOf(node); + } + + /** + *

inDegreeOf

+ * + * @param node a V object. + * @return a int. + */ + public int inDegreeOf(V node) { // TODO rename sth. like parentCount() + if (node == null || !containsVertex(node)) + return -1; + + return graph.inDegreeOf(node); + } + + // some queries + + /** + *

getEdge

+ * + * @param v1 a V object. + * @param v2 a V object. + * @return a E object. + */ + public E getEdge(V v1, V v2) { + return graph.getEdge(v1, v2); + } + + /** + *

containsVertex

+ * + * @param v a V object. + * @return a boolean. + */ + public boolean containsVertex(V v) { + // documentation says containsVertex() returns false on when given null + return graph.containsVertex(v); + } + + /** + *

containsEdge

+ * + * @param v1 a V object. + * @param v2 a V object. + * @return a boolean. + */ + public boolean containsEdge(V v1, V v2) { + return graph.containsEdge(v1, v2); + } + + /** + *

containsEdge

+ * + * @param e a E object. + * @return a boolean. + */ + public boolean containsEdge(E e) { + return graph.containsEdge(e); // TODO this seems to be buggy, at least + // for ControlFlowEdges + } + + /** + *

isEmpty

+ * + * @return a boolean. + */ + public boolean isEmpty() { + return graph.vertexSet().isEmpty(); + } + + /** + * Checks whether each vertex inside this graph is reachable from some other + * vertex + * + * @return a boolean. + */ + public boolean isConnected() { + if (vertexCount() < 2) + return true; + + V start = getRandomVertex(); + Set connectedToStart = determineConnectedVertices(start); + + return connectedToStart.size() == vertexSet().size(); + } + + /** + *

determineEntryPoints

+ * + * @return Set containing all nodes with in degree 0 + */ + public Set determineEntryPoints() { + Set r = new LinkedHashSet<>(); + + for (V instruction : vertexSet()) + if (inDegreeOf(instruction) == 0) { + r.add(instruction); + } + + return r; + } + + /** + *

determineExitPoints

+ * + * @return Set containing all nodes with out degree 0 + */ + public Set determineExitPoints() { + Set r = new LinkedHashSet<>(); + + for (V instruction : vertexSet()) + if (outDegreeOf(instruction) == 0) + r.add(instruction); + + return r; + } + + /** + * Follows all edges adjacent to the given vertex v ignoring edge directions + * and returns a set containing all vertices visited that way + * + * @param v a V object. + * @return a {@link java.util.Set} object. + */ + public Set determineConnectedVertices(V v) { + + Set visited = new LinkedHashSet<>(); + Queue queue = new LinkedList<>(); + + queue.add(v); + while (!queue.isEmpty()) { + V current = queue.poll(); + if (visited.contains(current)) + continue; + visited.add(current); + + queue.addAll(getParents(current)); + queue.addAll(getChildren(current)); + } + + return visited; + } + + /** + * Returns true iff whether the given node is not null, in this graph and + * has exactly n parents and m children. + * + * @param node a V object. + * @param n a int. + * @param m a int. + * @return a boolean. + */ + public boolean hasNPartentsMChildren(V node, int n, int m) { + if (node == null || !containsVertex(node)) + return false; + + return inDegreeOf(node) == n && outDegreeOf(node) == m; + } + + /** + * Returns a Set of all nodes within this graph that neither have incoming + * nor outgoing edges. + * + * @return a {@link java.util.Set} object. + */ + public Set getIsolatedNodes() { + Set r = new LinkedHashSet<>(); + for (V node : graph.vertexSet()) + if (inDegreeOf(node) == 0 && outDegreeOf(node) == 0) + r.add(node); + return r; + } + + /** + * Returns a Set containing every node in this graph that has no outgoing + * edges. + * + * @return a {@link java.util.Set} object. + */ + public Set getNodesWithoutChildren() { + Set r = new LinkedHashSet<>(); + for (V node : graph.vertexSet()) + if (outDegreeOf(node) == 0) + r.add(node); + return r; + } + + // utilities + + /** + *

getRandomVertex

+ * + * @return a V object. + */ + public V getRandomVertex() { + // TODO that's not really random + for (V v : graph.vertexSet()) + return v; + + return null; + } + + /** + *

getDistance

+ * + * @param v1 a V object. + * @param v2 a V object. + * @return a int. + */ + public int getDistance(V v1, V v2) { + DijkstraShortestPath d = new DijkstraShortestPath<>(graph, v1, v2); + return (int) Math.round(d.getPathLength()); + } + + /** + *

isDirectSuccessor

+ * + * @param v1 a V object. + * @param v2 a V object. + * @return a boolean. + */ + public boolean isDirectSuccessor(V v1, V v2) { + + return (containsEdge(v1, v2) && inDegreeOf(v2) == 1); + } + + // TODO make like determineEntry/ExitPoints + + /** + *

determineBranches

+ * + * @return a {@link java.util.Set} object. + */ + public Set determineBranches() { + return graph.vertexSet().stream() + .filter(instruction -> outDegreeOf(instruction) > 1) + .collect(toCollection(LinkedHashSet::new)); + } + + /** + *

determineJoins

+ * + * @return a {@link java.util.Set} object. + */ + public Set determineJoins() { + return vertexSet().stream() + .filter(instruction -> inDegreeOf(instruction) > 1) + .collect(toCollection(LinkedHashSet::new)); + } + + // building up the reverse graph + + /** + * Returns a reverted version of this graph in a jGraph + *

+ * That is a graph containing exactly the same nodes as this one but for + * each edge from v1 to v2 in this graph the resulting graph will contain an + * edge from v2 to v1 - or in other words the reverted edge + *

+ * This is used to revert CFGs in order to determine control dependencies + * for example + * + * @return a {@link org.jgrapht.graph.DefaultDirectedGraph} object. + */ + protected DefaultDirectedGraph computeReverseJGraph() { + + DefaultDirectedGraph r = new DefaultDirectedGraph<>(edgeClass); + + for (V v : vertexSet()) + if (!r.addVertex(v)) + throw new IllegalStateException( + "internal error while adding vertices"); + + for (E e : edgeSet()) { + V src = getEdgeSource(e); + V target = getEdgeTarget(e); + if (r.addEdge(target, src) == null) + throw new IllegalStateException( + "internal error while adding reverse edges"); + } + + return r; + } + + // visualizing the graph TODO clean up! + + /** + *

toDot

+ */ + public void toDot() { + + createGraphDirectory(); + + String dotFileName = getGraphDirectory() + toFileString(getName()) + + ".dot"; + toDot(dotFileName); + createToPNGScript(dotFileName); + } + + private String getGraphDirectory() { + return "evosuite-graphs/" + dotSubFolder(); + } + + /** + * Subclasses can overwrite this method in order to separate their .dot and + * .png export to a special folder. + * + * @return a {@link java.lang.String} object. + */ + protected String dotSubFolder() { + return ""; + } + + /** + *

toFileString

+ * + * @param name a {@link java.lang.String} object. + * @return a {@link java.lang.String} object. + */ + protected String toFileString(String name) { + + return name.replaceAll("\\(", "_").replaceAll("\\)", "_") + .replaceAll(";", "_").replaceAll("/", "_").replaceAll("<", "_") + .replaceAll(">", "_"); + } + + private void createGraphDirectory() { + + File graphDir = new File(getGraphDirectory()); + + if (!graphDir.exists() && !graphDir.mkdirs()) + throw new IllegalStateException("unable to create directory " + + getGraphDirectory()); + } + + private void createToPNGScript(String filename) { + File dotFile = new File(filename); + + // dot -Tpng RawCFG11_exe2_III_I.dot > file.png + assert (dotFile.exists() && !dotFile.isDirectory()); + + try { + String[] cmd = {"dot", "-Tpng", + "-o" + dotFile.getAbsolutePath() + ".png", + dotFile.getAbsolutePath()}; + Runtime.getRuntime().exec(cmd); + + } catch (IOException e) { + SimpleLogger.error("Problem while generating a graph for a dotFile", e); + } + } + + /** + *

getName

+ * + * @return a {@link java.lang.String} object. + */ + public String getName() { + return "EvoMasterGraph_" + graphId; + } + + /** + *

registerVertexAttributeProvider

+ * + * @param vertexAttributeProvider a {@link org.jgrapht.ext.ComponentAttributeProvider} object. + */ + public void registerVertexAttributeProvider( + ComponentAttributeProvider vertexAttributeProvider) { + this.vertexAttributeProvider = vertexAttributeProvider; + } + + /** + *

registerEdgeAttributeProvider

+ * + * @param edgeAttributeProvider a {@link org.jgrapht.ext.ComponentAttributeProvider} object. + */ + public void registerEdgeAttributeProvider( + ComponentAttributeProvider edgeAttributeProvider) { + this.edgeAttributeProvider = edgeAttributeProvider; + } + + private void toDot(String filename) { + + // TODO check if graphviz/dot is actually available on the current + // machine + + try { + + FileWriter fstream = new FileWriter(filename); + BufferedWriter out = new BufferedWriter(fstream); + if (!graph.vertexSet().isEmpty()) { + // FrameVertexNameProvider nameprovider = new + // FrameVertexNameProvider(mn.instructions); + // DOTExporter exporter = new + // DOTExporter(); + // DOTExporter exporter = new + // DOTExporter(new IntegerNameProvider(), + // nameprovider, new IntegerEdgeNameProvider()); + // DOTExporter exporter = new + // DOTExporter(new LineNumberProvider(), + // new LineNumberProvider(), new IntegerEdgeNameProvider()); + DOTExporter exporter = new DOTExporter<>( + new IntegerNameProvider<>(), + new StringNameProvider<>(), + new StringEdgeNameProvider<>(), + vertexAttributeProvider, edgeAttributeProvider); + + // new IntegerEdgeNameProvider()); + exporter.export(out, graph); + + SimpleLogger.info("exportet " + getName()); + } + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } +} diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/GraphPool.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/GraphPool.java new file mode 100755 index 0000000000..a0bc407a18 --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/GraphPool.java @@ -0,0 +1,452 @@ +/* + * Adapted from the EvoSuite project (https://github.com/EvoSuite/evosuite) + * and modified for use in EvoMaster's Dynamosa module. + */ +package org.evomaster.client.java.instrumentation.dynamosa.graphs; + +import org.evomaster.client.java.instrumentation.ClassesToExclude; +import com.google.common.annotations.VisibleForTesting; +import org.evomaster.client.java.instrumentation.dynamosa.DynamosaConfig; +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cdg.ControlDependenceGraph; +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.ActualControlFlowGraph; +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction; +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.ControlDependency; +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.RawControlFlowGraph; +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.branch.Branch; +import org.evomaster.client.java.instrumentation.external.DynamosaControlDependenceSnapshot; +import org.evomaster.client.java.instrumentation.shared.ClassName; +import org.evomaster.client.java.instrumentation.shared.dto.ControlDependenceGraphDto; +import org.evomaster.client.java.instrumentation.shared.dto.ControlDependenceGraphDto.BranchObjectiveDto; +import org.evomaster.client.java.instrumentation.shared.dto.ControlDependenceGraphDto.DependencyEdgeDto; +import org.evomaster.client.java.instrumentation.staticstate.ObjectiveRecorder; +import org.evomaster.client.java.utils.SimpleLogger; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Gives access to all Graphs computed during CUT analysis such as CFGs created + * by the CFGGenerator and BytcodeAnalyzer in the CFGMethodVisitor + *

+ * For each CUT and each of their methods a Raw- and an ActualControlFlowGraph + * instance are stored within this pool. Additionally a ControlDependenceGraph + * is computed and stored for each such method. + *

+ * This pool stores per-method CFGs and CDGs computed during analysis. + * + */ +public class GraphPool { + + private static final Map instanceMap = new HashMap<>(); + private static final List exportedCdgs = new ArrayList<>(); + private static final Object exportLock = new Object(); + + private final ClassLoader classLoader; + + /** + * Private constructor + */ + private GraphPool(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + private static boolean isWriteCfgEnabled() { + return DynamosaConfig.isWriteCfgEnabled(); + } + + public static GraphPool getInstance(ClassLoader classLoader) { + if (!instanceMap.containsKey(classLoader)) { + instanceMap.put(classLoader, new GraphPool(classLoader)); + } + + return instanceMap.get(classLoader); + } + + @VisibleForTesting + public static void resetForTesting(ClassLoader classLoader) { + instanceMap.remove(classLoader); + synchronized (exportLock) { + exportedCdgs.clear(); + } + } + + /** + * Complete control flow graph, contains each bytecode instruction, each + * label and line number node Think of the direct Known Subclasses of + * http:// + * asm.ow2.org/asm33/javadoc/user/org/objectweb/asm/tree/AbstractInsnNode + * .html for a complete list of the nodes in this cfg + *

+ * Maps from classNames to methodNames to corresponding RawCFGs + */ + private final Map> rawCFGs = new HashMap<>(); + + /** + * Minimized control flow graph. This graph only contains the first and last + * node (usually a LABEL and IRETURN), nodes which create branches (all + * jumps/switches except GOTO) and nodes which were mutated. + *

+ * Maps from classNames to methodNames to corresponding ActualCFGs + */ + private final Map> actualCFGs = new HashMap<>(); + + /** + * Control Dependence Graphs for each method. + *

+ * Maps from classNames to methodNames to corresponding CDGs + */ + private final Map> controlDependencies = new HashMap<>(); + + // retrieve graphs + + /** + * Returns the {@link RawControlFlowGraph} of the specified method. To this end, one has to + * provide + *

    + *
  • the fully qualified name of the class containing the desired method, and
  • + *
  • a string consisting of the method name concatenated with the corresponding + * method descriptor.
  • + *
+ * + * @param className the fully qualified name of the containing class + * @param methodName concatenation of method name and descriptor + * @return the raw control flow graph + */ + public RawControlFlowGraph getRawCFG(String className, String methodName) { + + if (rawCFGs.get(className) == null) { + SimpleLogger.warn("Class unknown: " + className); + SimpleLogger.warn(rawCFGs.keySet().toString()); + return null; + } + + return rawCFGs.get(className).get(methodName); + } + + /** + *

+ * getActualCFG + *

+ * + * @param className a {@link java.lang.String} object. + * @param methodName a {@link java.lang.String} object. + * @return a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.ActualControlFlowGraph} object. + */ + public ActualControlFlowGraph getActualCFG(String className, String methodName) { + + if (actualCFGs.get(className) == null) + return null; + + return actualCFGs.get(className).get(methodName); + } + + /** + *

+ * getCDG + *

+ * + * @param className a {@link java.lang.String} object. + * @param methodName a {@link java.lang.String} object. + * @return a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cdg.ControlDependenceGraph} object. + */ + public ControlDependenceGraph getCDG(String className, String methodName) { + + if (controlDependencies.get(className) == null) + return null; + + return controlDependencies.get(className).get(methodName); + } + + public static DynamosaControlDependenceSnapshot exportSnapshotFromIndex(int fromIndex) { + synchronized (exportLock) { + int start = Math.max(0, Math.min(fromIndex, exportedCdgs.size())); + List slice = new ArrayList<>(exportedCdgs.subList(start, exportedCdgs.size())); + return new DynamosaControlDependenceSnapshot(slice, exportedCdgs.size()); + } + } + + // register graphs + + /** + *

+ * registerRawCFG + *

+ * + * @param cfg a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.RawControlFlowGraph} object. + */ + public void registerRawCFG(RawControlFlowGraph cfg) { + String className = cfg.getClassName(); + String methodName = cfg.getMethodName(); + + if (className == null || methodName == null) + throw new IllegalStateException( + "expect class and method name of CFGs to be set before entering the GraphPool"); + + if (!rawCFGs.containsKey(className)) { + rawCFGs.put(className, new HashMap<>()); + } + Map methods = rawCFGs.get(className); + SimpleLogger.debug("Added complete CFG for class " + className + " and method " + + methodName); + methods.put(methodName, cfg); + + if (isWriteCfgEnabled()) + cfg.toDot(); + } + + /** + *

+ * registerActualCFG + *

+ * + * @param cfg a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.ActualControlFlowGraph} + * object. + */ + public void registerActualCFG(ActualControlFlowGraph cfg) { + String className = cfg.getClassName(); + String methodName = cfg.getMethodName(); + + if (className == null || methodName == null) + throw new IllegalStateException( + "expect class and method name of CFGs to be set before entering the GraphPool"); + + if (!actualCFGs.containsKey(className)) { + actualCFGs.put(className, new HashMap<>()); + // diameters.put(className, new HashMap()); + } + Map methods = actualCFGs.get(className); + SimpleLogger.debug("Added CFG for class " + className + " and method " + methodName); + methods.put(methodName, cfg); + + if (isWriteCfgEnabled()) + cfg.toDot(); + + if (shouldInstrument(cfg.getClassName(), cfg.getMethodName())) { + createAndRegisterControlDependence(cfg); + } + } + + private boolean shouldInstrument(String className, String methodName) { + return ClassesToExclude.checkIfCanInstrument(classLoader, new ClassName(className)); + } + + private void createAndRegisterControlDependence(ActualControlFlowGraph cfg) { + + ControlDependenceGraph cd = new ControlDependenceGraph(cfg); + + String className = cd.getClassName(); + String methodName = cd.getMethodName(); + + if (className == null || methodName == null) + throw new IllegalStateException( + "expect class and method name of CFGs to be set before entering the GraphPool"); + + if (!controlDependencies.containsKey(className)) + controlDependencies.put(className, + new HashMap<>()); + Map cds = controlDependencies.get(className); + + cds.put(methodName, cd); + if (isWriteCfgEnabled()) + cd.toDot(); + + ControlDependenceGraphDto dto = buildControlDependenceDto(className, methodName, cd); + if (dto != null) { + appendExportLog(dto); + } + } + + /** + *

+ * clear + *

+ */ + public void clear() { + rawCFGs.clear(); + actualCFGs.clear(); + controlDependencies.clear(); + } + + /** + *

+ * clear + *

+ * + * @param className a {@link java.lang.String} object. + */ + public void clear(String className) { + rawCFGs.remove(className); + actualCFGs.remove(className); + controlDependencies.remove(className); + } + + /** + *

+ * clear + *

+ * + * @param className a {@link java.lang.String} object. + * @param methodName a {@link java.lang.String} object. + */ + public void clear(String className, String methodName) { + if (rawCFGs.containsKey(className)) + rawCFGs.get(className).remove(methodName); + if (actualCFGs.containsKey(className)) + actualCFGs.get(className).remove(methodName); + if (controlDependencies.containsKey(className)) + controlDependencies.get(className).remove(methodName); + } + + public static void clearAll(String className) { + instanceMap.values().forEach(pool -> pool.clear(className)); + } + + public static void clearAll(String className, String methodName) { + instanceMap.values().forEach(pool -> pool.clear(className, methodName)); + } + + public static void clearAll() { + instanceMap.clear(); + synchronized (exportLock) { + exportedCdgs.clear(); + } + } + + public static void refreshAllCdgs() { + synchronized (exportLock) { + exportedCdgs.clear(); + } + instanceMap.values().forEach(GraphPool::refreshCdgs); + } + + private void refreshCdgs() { + for (String className : controlDependencies.keySet()) { + for (String methodName : controlDependencies.get(className).keySet()) { + ControlDependenceGraph cdg = controlDependencies.get(className).get(methodName); + ControlDependenceGraphDto dto = buildControlDependenceDto(className, methodName, cdg); + if (dto != null) { + appendExportLog(dto); + } + } + } + } + + private static void appendExportLog(ControlDependenceGraphDto dto) { + if (dto == null) { + return; + } + synchronized (exportLock) { + exportedCdgs.add(dto); + } + } + + private ControlDependenceGraphDto buildControlDependenceDto(String className, + String methodName, + ControlDependenceGraph cdg) { + if (cdg == null) { + return null; + } + ActualControlFlowGraph cfg = getActualCFG(className, methodName); + if (cfg == null) { + SimpleLogger.warn("Cannot export CDG for " + className + "#" + methodName + " as ActualCFG is missing"); + return null; + } + + Map objectiveMap = new LinkedHashMap<>(); + Set rootObjectives = new LinkedHashSet<>(); + Map> adjacency = new HashMap<>(); + + // Iterate over all branches in the ActualCFG. + for (BytecodeInstruction branchInstruction : cfg.getBranches()) { + // Branches are stored in the BranchPool as they are instrumented. + // Here we retrieve the Branch object from the BranchPool, by its BytecodeInstruction. + // Branch also stores the descriptive identifiers for the "true" and "false" outcomes. + Branch branch = branchInstruction.toBranch(); + if (branch == null) { + continue; + } + + + List branchObjectives = collectObjectiveIds(branch, objectiveMap); + if (branchObjectives.isEmpty()) { + continue; + } + + Set dependencies = branchInstruction.getControlDependencies(); + if (dependencies == null || dependencies.isEmpty()) { + rootObjectives.addAll(branchObjectives); + continue; + } + + for (ControlDependency dependency : dependencies) { + Branch parent = dependency.getBranch(); + if (parent == null) { + continue; + } + Integer parentObjective = resolveParentObjectiveId(parent, dependency.getBranchExpressionValue(), objectiveMap); + if (parentObjective == null) { + continue; + } + + adjacency.computeIfAbsent(parentObjective, key -> new LinkedHashSet<>()) + .addAll(branchObjectives); + } + } + + if (objectiveMap.isEmpty() && adjacency.isEmpty()) { + return null; + } + + List edges = new ArrayList<>(); + adjacency.forEach((parent, childrenIds) -> { + for (Integer child : childrenIds) { + edges.add(new DependencyEdgeDto(parent, child)); + } + }); + + ControlDependenceGraphDto dto = new ControlDependenceGraphDto(); + dto.setClassName(className); + dto.setMethodName(methodName); + dto.setObjectives(new ArrayList<>(objectiveMap.values())); + dto.setRootObjectiveIds(new ArrayList<>(rootObjectives)); + dto.setEdges(edges); + return dto; + } + + private List collectObjectiveIds(Branch branch, + Map objectiveMap) { + List ids = new ArrayList<>(2); + if (branch.getThenObjectiveId() != null) { + ids.add(registerObjective(branch.getThenObjectiveId(), objectiveMap)); + } + if (branch.getElseObjectiveId() != null) { + ids.add(registerObjective(branch.getElseObjectiveId(), objectiveMap)); + } + return ids; + } + + private Integer resolveParentObjectiveId(Branch branch, + boolean branchExpressionValue, + Map objectiveMap) { + String descriptiveId = branchExpressionValue + ? branch.getThenObjectiveId() + : branch.getElseObjectiveId(); + if (descriptiveId == null) { + return null; + } + return registerObjective(descriptiveId, objectiveMap); + } + + private Integer registerObjective(String descriptiveId, + Map objectiveMap) { + int id = ObjectiveRecorder.getMappedId(descriptiveId); + objectiveMap.computeIfAbsent(id, key -> new BranchObjectiveDto(id, descriptiveId)); + return id; + } + +} diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cdg/ControlDependenceGraph.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cdg/ControlDependenceGraph.java new file mode 100755 index 0000000000..560fcbdb2a --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cdg/ControlDependenceGraph.java @@ -0,0 +1,219 @@ +/* + * Adapted from the EvoSuite project (https://github.com/EvoSuite/evosuite) + * and modified for use in EvoMaster's Dynamosa module. + */ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cdg; + +import org.evomaster.client.java.instrumentation.dynamosa.graphs.EvoMasterGraph; +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.*; +import org.evomaster.client.java.utils.SimpleLogger; + +import java.util.LinkedHashSet; +import java.util.Set; + +public class ControlDependenceGraph extends EvoMasterGraph { + + private final ActualControlFlowGraph cfg; + + private final String className; + private final String methodName; + + /** + *

Constructor for ControlDependenceGraph.

+ * + * @param cfg a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.ActualControlFlowGraph} object. + */ + public ControlDependenceGraph(ActualControlFlowGraph cfg) { + super(ControlFlowEdge.class); + + this.cfg = cfg; + this.className = cfg.getClassName(); + this.methodName = cfg.getMethodName(); + + computeGraph(); + } + + + /** + * Returns a Set containing all Branches the given BasicBlock is control + * dependent on. + *

+ * This is for each incoming ControlFlowEdge of the given block within this + * CDG, the branch instruction of that edge will be added to the returned + * set. + * + * @param insBlock a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BasicBlock} object. + * @return a {@link java.util.Set} object. + */ + public Set getControlDependentBranches(BasicBlock insBlock) { + if (insBlock == null) + throw new IllegalArgumentException("null not accepted"); + if (!containsVertex(insBlock)) + throw new IllegalArgumentException("unknown block: " + insBlock.getName()); + + if (insBlock.hasControlDependenciesSet()) + return insBlock.getControlDependencies(); + + Set r = retrieveControlDependencies(insBlock, + new LinkedHashSet<>()); + + return r; + } + + private Set retrieveControlDependencies(BasicBlock insBlock, + Set handled) { + + Set r = new LinkedHashSet<>(); + + for (ControlFlowEdge e : incomingEdgesOf(insBlock)) { + if (handled.contains(e)) + continue; + handled.add(e); + + ControlDependency cd = e.getControlDependency(); + if (cd != null) + r.add(cd); + else { + BasicBlock in = getEdgeSource(e); + if (!in.equals(insBlock)) + r.addAll(retrieveControlDependencies(in, handled)); + } + + } + + return r; + } + + // init + + private void computeGraph() { + + createGraphNodes(); + computeControlDependence(); + } + + private void createGraphNodes() { + // copy CFG nodes + addVertices(cfg.vertexSet()); + + for (BasicBlock b : vertexSet()) + if (b.isExitBlock() && !graph.removeVertex(b)) // TODO refactor + throw new IllegalStateException("internal error building up CDG"); + + } + + private void computeControlDependence() { + + ActualControlFlowGraph rcfg = cfg.computeReverseCFG(); + DominatorTree dt = new DominatorTree<>(rcfg); + + for (BasicBlock b : rcfg.vertexSet()) + if (!b.isExitBlock()) { + + SimpleLogger.debug("DFs for: " + b.getName()); + for (BasicBlock cd : dt.getDominatingFrontiers(b)) { + ControlFlowEdge orig = cfg.getEdge(cd, b); + + if (!cd.isEntryBlock() && orig == null) { + // in for loops for example it can happen that cd and b + // were not directly adjacent to each other in the CFG + // but rather there were some intermediate nodes between + // them and the needed information is inside one of the + // edges + // from cd to the first intermediate node. more + // precisely cd is expected to be a branch and to have 2 + // outgoing edges, one for evaluating to true (jumping) + // and one for false. one of them can be followed and b + // will eventually be reached, the other one can not be + // followed in that way. + + SimpleLogger.debug("cd: " + cd); + SimpleLogger.debug("b: " + b); + + // TODO this is just for now! unsafe and probably not + // even correct! + Set candidates = cfg.outgoingEdgesOf(cd); + if (candidates.size() < 2) + throw new IllegalStateException("unexpected"); + + boolean leadToB = false; + boolean skip = false; + + for (ControlFlowEdge e : candidates) { + if (!e.hasControlDependency()) { + skip = true; + break; + } + + if (cfg.leadsToNode(e, b)) { + if (leadToB) + orig = null; + // throw new + // IllegalStateException("unexpected"); + leadToB = true; + + orig = e; + } + } + if (skip) + continue; + if (!leadToB) + throw new IllegalStateException("unexpected"); + } + + if (orig == null) + SimpleLogger.debug("orig still null!"); + + if (!addEdge(cd, b, new ControlFlowEdge(orig))) + throw new IllegalStateException( + "internal error while adding CD edge"); + + SimpleLogger.debug(" " + cd.getName()); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + // return "CDG" + graphId + "_" + methodName; + return methodName + "_" + "CDG"; + } + + /** + * {@inheritDoc} + */ + @Override + protected String dotSubFolder() { + return toFileString(className) + "/CDG/"; + } + + /** + *

Getter for the field className.

+ * + * @return a {@link java.lang.String} object. + */ + public String getClassName() { + return className; + } + + /** + *

Getter for the field methodName.

+ * + * @return a {@link java.lang.String} object. + */ + public String getMethodName() { + return methodName; + } + + /** + * Exposes the underlying {@link ActualControlFlowGraph} + * + * @return the CFG used to build this CDG. + */ + public ActualControlFlowGraph getCFG() { + return cfg; + } +} diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cdg/DominatorNode.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cdg/DominatorNode.java new file mode 100755 index 0000000000..8d4ba72c45 --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cdg/DominatorNode.java @@ -0,0 +1,99 @@ +/* + * Adapted from the EvoSuite project (https://github.com/EvoSuite/evosuite) + * and modified for use in EvoMaster's Dynamosa module. + */ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cdg; + +import java.util.HashSet; +import java.util.Set; + +/** + * This class serves as a convenience data structure within cfg.DominatorTree + *

+ * For every node within a CFG for which the immediateDominators are to be + * computed this class holds auxiliary information needed during the computation + * inside the DominatorTree + *

+ * After that computation instances of this class hold the connection between + * CFG nodes and their immediateDominators + *

+ * Look at cfg.DominatorTree for more detailed information + * + */ +class DominatorNode { + + final V node; + int n = 0; + + // parent of node within spanning tree of DFS inside cfg.DominatorTree + DominatorNode parent; + + // computed dominators + DominatorNode semiDominator; + DominatorNode immediateDominator; + + // auxiliary field needed for dominator computation + Set> bucket = new HashSet<>(); + + // data structure needed to represented forest produced during cfg.DominatorTree computation + DominatorNode ancestor; + DominatorNode label; + + DominatorNode(V node) { + this.node = node; + + this.label = this; + } + + void link(DominatorNode v) { + ancestor = v; + } + + DominatorNode eval() { + if (ancestor == null) + return this; + + compress(); + + return label; + } + + void compress() { + if (ancestor == null) + throw new IllegalStateException("may only be called when ancestor is set"); + + if (ancestor.ancestor != null) { + ancestor.compress(); + if (ancestor.label.semiDominator.n < label.semiDominator.n) + label = ancestor.label; + + ancestor = ancestor.ancestor; + } + } + + DominatorNode getFromBucket() { + + for (DominatorNode r : bucket) + return r; + + return null; + } + + /** + *

isRootNode

+ * + * @return a boolean. + */ + public boolean isRootNode() { + // TODO not that nice :/ + return n == 1; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return "DTNode " + n + " - " + node; + } +} diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cdg/DominatorTree.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cdg/DominatorTree.java new file mode 100755 index 0000000000..7e51026f58 --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cdg/DominatorTree.java @@ -0,0 +1,294 @@ +/* + * Adapted from the EvoSuite project (https://github.com/EvoSuite/evosuite) + * and modified for use in EvoMaster's Dynamosa module. + */ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cdg; + +import org.evomaster.client.java.instrumentation.dynamosa.graphs.EvoMasterGraph; +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.ControlFlowGraph; +import org.jgrapht.graph.DefaultEdge; +import org.evomaster.client.java.utils.SimpleLogger; + +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + + +/** + * Given a CFG this class computes the immediateDominators and the + * dominatingFrontiers for each CFG vertex + *

+ * The current algorithm to determine the immediateDominators runs in time + * O(e*log n) where e is the number of control flow edges and n the number of + * CFG vertices and is taken from: + *

+ * "A Fast Algorithm for Finding Dominators in a Flowgraph" THOMAS LENGAUER and + * ROBERT ENDRE TARJAN 1979, Stanford University + *

+ * DOI: 10.1145/357062.357071 + * http://portal.acm.org/citation.cfm?doid=357062.357071 + *

+ *

+ * The algorithm for computing the dominatingFrontiers when given the + * immediateDominators is taken from + *

+ * "Efficiently Computing Static Single Assignment Form and the Control + * Dependence Graph" RON CYTRON, JEANNE FERRANTE, BARRY K. ROSEN, and MARK N. + * WEGMAN IBM Research Division and F. KENNETH ZADECK Brown University 1991 + * + */ +public class DominatorTree extends EvoMasterGraph, DefaultEdge> { + + private int nodeCount = 0; + private final ControlFlowGraph cfg; + + private final Map> dominatorNodesMap = new LinkedHashMap<>(); + private final Map> dominatorIDMap = new LinkedHashMap<>(); + private final Map> dominatingFrontiers = new LinkedHashMap<>(); + + /** + * Will start the computation of all immediateDominators for the given CFG + * which can later be retrieved via getImmediateDominator() + * + * @param cfg a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.ControlFlowGraph} object. + */ + public DominatorTree(ControlFlowGraph cfg) { + super(DefaultEdge.class); + + SimpleLogger.debug("Computing DominatorTree for " + cfg.getName()); + + this.cfg = cfg; + + createDominatorNodes(); + + V root = cfg.determineEntryPoint(); // TODO change to getEntryPoint() + SimpleLogger.debug("determined root: " + root); + DominatorNode rootNode = getDominatorNodeFor(root); + + depthFirstAnalyze(rootNode); + + computeSemiDominators(); + computeImmediateDominators(rootNode); + + createDominatorTree(); + + computeDominatorFrontiers(rootNode); + + // toDot(); + } + + private void createDominatorTree() { + + // add dominator nodes + addVertices(dominatorIDMap.values()); + + SimpleLogger.debug("DTNodes: " + vertexCount()); + + // build up tree by adding for each node v an edge from v.iDom to v + for (DominatorNode v : vertexSet()) { + if (v.isRootNode()) + continue; + if (addEdge(v.immediateDominator, v) == null) + throw new IllegalStateException( + "internal error while building dominator tree edges"); + + SimpleLogger.debug("added DTEdge from " + v.immediateDominator.n + " to " + v.n); + } + + SimpleLogger.debug("DTEdges: " + edgeCount()); + + // sanity check + if (isEmpty()) + throw new IllegalStateException("expect dominator trees to not be empty"); + // check tree is connected + if (!isConnected()) + throw new IllegalStateException("dominator tree expected to be connected"); + } + + private void computeDominatorFrontiers(DominatorNode currentNode) { + + // TODO check assumption: exitPoints in original CFG are exitPoints in resulting DominatorTree + + for (DominatorNode child : getChildren(currentNode)) + computeDominatorFrontiers(child); + + SimpleLogger.debug("computing dominatingFrontier for: " + currentNode.toString()); + + Set dominatingFrontier = dominatingFrontiers.get(currentNode); + if (dominatingFrontier == null) + dominatingFrontier = new HashSet<>(); + + // "local" + for (V child : cfg.getChildren(currentNode.node)) { + DominatorNode y = getDominatorNodeFor(child); + if (y.immediateDominator.n != currentNode.n) { + SimpleLogger.debug(" LOCAL adding to DFs: " + y.node); + dominatingFrontier.add(y.node); + } + } + + // "up" + for (DominatorNode z : getChildren(currentNode)) + for (V y : dominatingFrontiers.get(z.node)) + if (getDominatorNodeFor(y).immediateDominator.n != currentNode.n) { + SimpleLogger.debug(" UP adding to DFs: " + y); + dominatingFrontier.add(y); + } + + dominatingFrontiers.put(currentNode.node, dominatingFrontier); + } + + /** + * Given a node of this objects CFG this method returns it's previously + * computed immediateDominator + *

+ * The immediateDominator iDom of a node v has the following properties: + *

+ * 1) iDom dominates v + *

+ * 2) every other dominator of v dominates iDom + *

+ * A node w dominates v or is a dominator of v if and only if every path + * from the CFG's entryPoint to v contains w + * + * @param v A node within this objects CFG for wich the immediateDominator + * is to be returned + * @return a V object. + */ + public V getImmediateDominator(V v) { + if (v == null) + throw new IllegalArgumentException("null given"); + DominatorNode domNode = dominatorNodesMap.get(v); + if (domNode == null) + throw new IllegalStateException("unknown vertice given"); + + if (domNode.immediateDominator == null) { + // sanity check: this is only allowed to happen if v is root of CFG + if (domNode.n != 1) + throw new IllegalStateException( + "expect known node without iDom to be root of CFG"); + + return null; + } + + return domNode.immediateDominator.node; + } + + /** + *

Getter for the field dominatingFrontiers.

+ * + * @param v a V object. + * @return a {@link java.util.Set} object. + */ + public Set getDominatingFrontiers(V v) { + if (v == null) + throw new IllegalStateException("null given"); + + return dominatingFrontiers.get(v); + } + + // computation + + private void createDominatorNodes() { + + for (V v : cfg.vertexSet()) + dominatorNodesMap.put(v, new DominatorNode<>(v)); + } + + private void depthFirstAnalyze(DominatorNode currentNode) { + // step 1 + + initialize(currentNode); + + for (V w : cfg.getChildren(currentNode.node)) { + DominatorNode wNode = getDominatorNodeFor(w); + if (wNode.semiDominator == null) { + wNode.parent = currentNode; + depthFirstAnalyze(wNode); + } + } + } + + private void initialize(DominatorNode currentNode) { + + nodeCount++; + currentNode.n = nodeCount; + currentNode.semiDominator = currentNode; + + SimpleLogger.debug("created " + currentNode + " for " + + currentNode.node.toString()); + + dominatorIDMap.put(nodeCount, currentNode); + } + + private void computeSemiDominators() { + + for (int i = nodeCount; i >= 2; i--) { + DominatorNode w = getDominatorNodeById(i); + + // step 2 + for (V current : cfg.getParents(w.node)) { + DominatorNode v = getDominatorNodeFor(current); + DominatorNode u = v.eval(); + + if (u.semiDominator.n < w.semiDominator.n) + w.semiDominator = u.semiDominator; + } + + w.semiDominator.bucket.add(w); + w.link(w.parent); + + // step 3 + while (!w.parent.bucket.isEmpty()) { + + DominatorNode v = w.parent.getFromBucket(); + if (!w.parent.bucket.remove(v)) + throw new IllegalStateException("internal error"); + + DominatorNode u = v.eval(); + v.immediateDominator = (u.semiDominator.n < v.semiDominator.n ? u + : w.parent); + } + } + } + + private void computeImmediateDominators(DominatorNode rootNode) { + // step 4 + for (int i = 2; i <= nodeCount; i++) { + DominatorNode w = getDominatorNodeById(i); + + if (w.immediateDominator != w.semiDominator) + w.immediateDominator = w.immediateDominator.immediateDominator; + + // logger.debug("iDom for node "+i+" was: "+w.immediateDominator.n); + } + + rootNode.immediateDominator = null; + } + + private DominatorNode getDominatorNodeById(int id) { + DominatorNode r = dominatorIDMap.get(id); + if (r == null) + throw new IllegalArgumentException("id unknown to this tree"); + + return r; + } + + private DominatorNode getDominatorNodeFor(V v) { + DominatorNode r = dominatorNodesMap.get(v); + if (r == null) + throw new IllegalStateException( + "expect dominatorNodesMap to contain domNodes for all Vs"); + + return r; + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return "DominatorTree" + graphId; + } +} diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/ActualControlFlowGraph.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/ActualControlFlowGraph.java new file mode 100755 index 0000000000..07ee96ad97 --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/ActualControlFlowGraph.java @@ -0,0 +1,529 @@ +/* + * Adapted from the EvoSuite project (https://github.com/EvoSuite/evosuite) + * and modified for use in EvoMaster's Dynamosa module. + */ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg; + +import org.evomaster.client.java.utils.SimpleLogger; + +import java.util.HashSet; +import java.util.Set; + +public class ActualControlFlowGraph extends ControlFlowGraph { + + private RawControlFlowGraph rawGraph; + + private BytecodeInstruction entryPoint; + private Set exitPoints; + private Set branches; + private Set branchTargets; + private Set joins; + private Set joinSources; + + /** + *

+ * Constructor for ActualControlFlowGraph. + *

+ * + * @param rawGraph a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.RawControlFlowGraph} object. + */ + public ActualControlFlowGraph(RawControlFlowGraph rawGraph) { + super(rawGraph.getClassName(), rawGraph.getMethodName(), + rawGraph.getMethodAccess()); + + this.rawGraph = rawGraph; + + fillSets(); + computeGraph(); + } + + /** + *

+ * Constructor for ActualControlFlowGraph. + *

+ * + * @param toRevert a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.ActualControlFlowGraph} + * object. + */ + protected ActualControlFlowGraph(ActualControlFlowGraph toRevert) { + super(toRevert.className, toRevert.methodName, toRevert.access, + toRevert.computeReverseJGraph()); + } + + /** + *

+ * computeReverseCFG + *

+ * + * @return a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.ActualControlFlowGraph} object. + */ + public ActualControlFlowGraph computeReverseCFG() { + // TODO: this must be possible to "pre implement" in EvoMasterGraph for + // all sub class of EvoMasterGraph + return new ActualControlFlowGraph(this); + } + + // initialization + + private void fillSets() { + + setEntryPoint(rawGraph.determineEntryPoint()); + setExitPoints(rawGraph.determineExitPoints()); + + setBranches(rawGraph.determineBranches()); + setBranchTargets(); + setJoins(rawGraph.determineJoins()); + setJoinSources(); + } + + private void setEntryPoint(BytecodeInstruction entryPoint) { + if (entryPoint == null) + throw new IllegalArgumentException("null given"); + if (!belongsToMethod(entryPoint)) + throw new IllegalArgumentException( + "entry point does not belong to this CFGs method"); + this.entryPoint = entryPoint; + } + + private void setExitPoints(Set exitPoints) { + if (exitPoints == null) + throw new IllegalArgumentException("null given"); + + this.exitPoints = new HashSet<>(); + + for (BytecodeInstruction exitPoint : exitPoints) { + if (!belongsToMethod(exitPoint)) + throw new IllegalArgumentException( + "exit point does not belong to this CFGs method"); + if (!exitPoint.canBeExitPoint()) + throw new IllegalArgumentException( + "unexpected exitPoint byteCode instruction type: " + + exitPoint.getInstructionType()); + + this.exitPoints.add(exitPoint); + } + } + + private void setJoins(Set joins) { + if (joins == null) + throw new IllegalArgumentException("null given"); + + this.joins = new HashSet<>(); + + for (BytecodeInstruction join : joins) { + if (!belongsToMethod(join)) + throw new IllegalArgumentException( + "join does not belong to this CFGs method"); + + this.joins.add(join); + } + } + + private void setJoinSources() { + if (joins == null) + throw new IllegalStateException( + "expect joins to be set before setting of joinSources"); + if (rawGraph == null) + throw new IllegalArgumentException("null given"); + + this.joinSources = new HashSet<>(); + + for (BytecodeInstruction join : joins) + for (ControlFlowEdge joinEdge : rawGraph.incomingEdgesOf(join)) + joinSources.add(rawGraph.getEdgeSource(joinEdge)); + } + + private void setBranches(Set branches) { + if (branches == null) + throw new IllegalArgumentException("null given"); + + this.branches = new HashSet<>(); + + for (BytecodeInstruction branch : branches) { + if (!belongsToMethod(branch)) + throw new IllegalArgumentException( + "branch does not belong to this CFGs method"); + + this.branches.add(branch); + } + } + + private void setBranchTargets() { + if (branches == null) + throw new IllegalStateException( + "expect branches to be set before setting of branchTargets"); + if (rawGraph == null) + throw new IllegalArgumentException("null given"); + + this.branchTargets = new HashSet<>(); + + for (BytecodeInstruction branch : branches) + for (ControlFlowEdge branchEdge : rawGraph.outgoingEdgesOf(branch)) + branchTargets.add(rawGraph.getEdgeTarget(branchEdge)); + } + + private Set getInitiallyKnownInstructions() { + Set r = new HashSet<>(); + r.add(entryPoint); + r.addAll(exitPoints); + r.addAll(branches); + r.addAll(branchTargets); + r.addAll(joins); + r.addAll(joinSources); + + return r; + } + + // compute actual CFG from RawControlFlowGraph + + private void computeGraph() { + + computeNodes(); + computeEdges(); + + addAuxiliaryBlocks(); + } + + private void addAuxiliaryBlocks() { + + // TODO clean up mess: exit-/entry- POINTs versus BLOCKs + + EntryBlock entry = new EntryBlock(className, methodName); + ExitBlock exit = new ExitBlock(className, methodName); + + addBlock(entry); + addBlock(exit); + addEdge(entry, exit); + addEdge(entry, this.entryPoint.getBasicBlock()); + for (BytecodeInstruction exitPoint : this.exitPoints) { + addEdge(exitPoint.getBasicBlock(), exit); + } + } + + private void computeNodes() { + + Set nodes = getInitiallyKnownInstructions(); + + for (BytecodeInstruction node : nodes) { + if (knowsInstruction(node)) + continue; + + BasicBlock nodeBlock = rawGraph.determineBasicBlockFor(node); + addBlock(nodeBlock); + } + + SimpleLogger.debug(vertexCount() + " BasicBlocks"); + } + + private void computeEdges() { + + for (BasicBlock block : vertexSet()) { + + computeIncomingEdgesFor(block); + computeOutgoingEdgesFor(block); + } + + SimpleLogger.debug(edgeCount() + " ControlFlowEdges"); + } + + private void computeIncomingEdgesFor(BasicBlock block) { + + if (isEntryPoint(block)) + return; + + BytecodeInstruction blockStart = block.getFirstInstruction(); + Set rawIncomings = rawGraph.incomingEdgesOf(blockStart); + for (ControlFlowEdge rawIncoming : rawIncomings) { + BytecodeInstruction incomingStart = rawGraph.getEdgeSource(rawIncoming); + addRawEdge(incomingStart, block, rawIncoming); + } + } + + private void computeOutgoingEdgesFor(BasicBlock block) { + + if (isExitPoint(block)) + return; + + BytecodeInstruction blockEnd = block.getLastInstruction(); + + Set rawOutgoings = rawGraph.outgoingEdgesOf(blockEnd); + for (ControlFlowEdge rawOutgoing : rawOutgoings) { + BytecodeInstruction outgoingEnd = rawGraph.getEdgeTarget(rawOutgoing); + addRawEdge(block, outgoingEnd, rawOutgoing); + } + } + + // internal graph handling + + /** + *

+ * addBlock + *

+ * + * @param nodeBlock a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BasicBlock} object. + */ + protected void addBlock(BasicBlock nodeBlock) { + if (nodeBlock == null) + throw new IllegalArgumentException("null given"); + + SimpleLogger.debug("Adding block: " + nodeBlock.getName()); + + if (containsVertex(nodeBlock)) + throw new IllegalArgumentException("block already added before"); + + if (!addVertex(nodeBlock)) + throw new IllegalStateException( + "internal error while addind basic block to CFG"); + + + if (!containsVertex(nodeBlock)) + throw new IllegalStateException( + "expect graph to contain the given block on returning of addBlock()"); + + SimpleLogger.debug(".. succeeded. nodeCount: " + vertexCount()); + } + + /** + *

+ * addRawEdge + *

+ * + * @param src a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction} object. + * @param target a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BasicBlock} object. + * @param origEdge a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.ControlFlowEdge} object. + */ + protected void addRawEdge(BytecodeInstruction src, BasicBlock target, + ControlFlowEdge origEdge) { + BasicBlock srcBlock = src.getBasicBlock(); + if (srcBlock == null) + throw new IllegalStateException( + "when adding an edge to a CFG it is expected to know both the src- and the target-instruction"); + + addRawEdge(srcBlock, target, origEdge); + } + + /** + *

+ * addRawEdge + *

+ * + * @param src a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BasicBlock} object. + * @param target a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction} object. + * @param origEdge a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.ControlFlowEdge} object. + */ + protected void addRawEdge(BasicBlock src, BytecodeInstruction target, + ControlFlowEdge origEdge) { + BasicBlock targetBlock = target.getBasicBlock(); + if (targetBlock == null) + throw new IllegalStateException( + "when adding an edge to a CFG it is expected to know both the src- and the target-instruction"); + + addRawEdge(src, targetBlock, origEdge); + } + + /** + *

+ * addRawEdge + *

+ * + * @param src a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BasicBlock} object. + * @param target a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BasicBlock} object. + * @param origEdge a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.ControlFlowEdge} object. + */ + protected void addRawEdge(BasicBlock src, BasicBlock target, ControlFlowEdge origEdge) { + if (src == null || target == null) + throw new IllegalArgumentException("null given"); + + SimpleLogger.debug("Adding edge from " + src.getName() + " to " + target.getName()); + + if (containsEdge(src, target)) { + SimpleLogger.debug("edge already contained in CFG"); + // sanity check + ControlFlowEdge current = getEdge(src, target); + if (current == null) + throw new IllegalStateException( + "expect getEdge() not to return null on parameters on which containsEdge() retruned true"); + if (current.getBranchExpressionValue() + && !origEdge.getBranchExpressionValue()) + throw new IllegalStateException( + "if this rawEdge was handled before i expect the old edge to have same branchExpressionValue set"); + if (current.getBranchInstruction() == null) { + if (origEdge.getBranchInstruction() != null) + throw new IllegalStateException( + "if this rawEdge was handled before i expect the old edge to have same branchInstruction set"); + + } else if (origEdge.getBranchInstruction() == null + || !current.getBranchInstruction().equals(origEdge.getBranchInstruction())) + throw new IllegalStateException( + "if this rawEdge was handled before i expect the old edge to have same branchInstruction set"); + + return; + } + + ControlFlowEdge e = new ControlFlowEdge(origEdge); + if (!super.addEdge(src, target, e)) + throw new IllegalStateException("internal error while adding edge to CFG"); + + SimpleLogger.debug(".. succeeded, edgeCount: " + edgeCount()); + } + + // convenience methods to switch between BytecodeInstructons and BasicBlocks + + /** + * If the given instruction is known to this graph, the BasicBlock holding + * that instruction is returned. Otherwise null will be returned. + * + * @param instruction a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction} object. + * @return a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BasicBlock} object. + */ + public BasicBlock getBlockOf(BytecodeInstruction instruction) { + if (instruction == null) + throw new IllegalArgumentException("null given"); + + if (instruction.hasBasicBlockSet()) + return instruction.getBasicBlock(); + + for (BasicBlock block : vertexSet()) + if (block.containsInstruction(instruction)) { + instruction.setBasicBlock(block); + return block; + } + + SimpleLogger.debug("unknown instruction " + instruction); + return null; + } + + /** + * Checks whether this graph knows the given instruction. That is there is a + * BasicBlock in this graph's vertexSet containing the given instruction. + * + * @param instruction a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction} object. + * @return a boolean. + */ + public boolean knowsInstruction(BytecodeInstruction instruction) { + if (instruction == null) + throw new IllegalArgumentException("null given"); + + if (instruction.hasBasicBlockSet()) + return containsVertex(instruction.getBasicBlock()); + + for (BasicBlock block : vertexSet()) + if (block.containsInstruction(instruction)) + return true; + + return false; + } + + /** + *

+ * getDistance + *

+ * + * @param v1 a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction} object. + * @param v2 a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction} object. + * @return a int. + */ + /** + *

+ * isEntryPoint + *

+ * + * @param block a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BasicBlock} object. + * @return a boolean. + */ + private boolean isEntryPoint(BasicBlock block) { + if (block == null) + throw new IllegalArgumentException("null given"); + + return block.containsInstruction(entryPoint); + } + + /** + *

+ * isExitPoint + *

+ * + * @param block a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BasicBlock} object. + * @return a boolean. + */ + private boolean isExitPoint(BasicBlock block) { + if (block == null) + throw new IllegalArgumentException("null given"); + + for (BytecodeInstruction exitPoint : exitPoints) + if (block.containsInstruction(exitPoint)) { + return true; + } + + return false; + } + + /** + *

+ * belongsToMethod + *

+ * + * @param instruction a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction} object. + * @return a boolean. + */ + private boolean belongsToMethod(BytecodeInstruction instruction) { + if (instruction == null) + throw new IllegalArgumentException("null given"); + + if (!className.equals(instruction.getClassName())) + return false; + return methodName.equals(instruction.getMethodName()); + } + + // inherited from ControlFlowGraph + + /** + * {@inheritDoc} + */ + @Override + public boolean containsInstruction(BytecodeInstruction v) { + if (v == null) + return false; + + for (BasicBlock block : vertexSet()) + if (block.containsInstruction(v)) + return true; + + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public BytecodeInstruction getInstruction(int instructionId) { + + BytecodeInstruction searchedFor = BytecodeInstructionPool.getInstance(rawGraph.getClassLoader()).getInstruction(className, + methodName, + instructionId); + + if (containsInstruction(searchedFor)) + return searchedFor; + + return null; + } + + /** + *

+ * Getter for the field branches. + *

+ * + * @return a {@link java.util.Set} object. + */ + public Set getBranches() { + return new HashSet<>(branches); + } + + /** + * {@inheritDoc} + */ + @Override + public String getCFGType() { + return "ACFG"; + } +} diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/BasicBlock.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/BasicBlock.java new file mode 100755 index 0000000000..b0251a5125 --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/BasicBlock.java @@ -0,0 +1,402 @@ +/* + * Adapted from the EvoSuite project (https://github.com/EvoSuite/evosuite) + * and modified for use in EvoMaster's Dynamosa module. + */ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg; + +import org.evomaster.client.java.instrumentation.dynamosa.graphs.GraphPool; +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cdg.ControlDependenceGraph; + +import java.io.Serializable; +import java.util.*; + +/** + * This class is used to represent basic blocks in the control flow graph. + *

+ * A basic block is a list of instructions for which the following holds: + *

+ * Whenever control flow reaches the first instruction of this blocks list, + * control flow will pass through all the instructions of this list successively + * and not pass another instruction of the underlying method in the mean time. + * The first element in this blocks list does not have a parent in the CFG that + * can be prepended to the list and the same would still hold true Finally the + * last element in this list does not have a child inside the CFG that could be + * appended to the list such that the above still holds true + *

+ * In other words: - the first/last element of this blocks list has either 0 or + * >=2 parents/children in the CFG - every other element in the list has exactly + * 1 parent and exactly 1 child in the raw CFG + *

+ *

+ * Taken from: + *

+ * "Efficiently Computing Static Single Assignment Form and the Control + * Dependence Graph" RON CYTRON, JEANNE FERRANTE, BARRY K. ROSEN, and MARK N. + * WEGMAN IBM Research Division and F. KENNETH ZADECK Brown University 1991 + * + * @see org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.ActualControlFlowGraph + */ +public class BasicBlock implements Serializable, Iterable { + + private static final long serialVersionUID = -3465486470017841484L; + + private static int blockCount = 0; + + private int id = -1; + protected ClassLoader classLoader; + protected String className; + protected String methodName; + + private Set controlDependencies; + + protected boolean isAuxiliaryBlock = false; + + private final List instructions = new ArrayList<>(); + + /** + *

+ * Constructor for BasicBlock. + *

+ * + * @param className a {@link java.lang.String} object. + * @param methodName a {@link java.lang.String} object. + * @param blockNodes a {@link java.util.List} object. + */ + public BasicBlock(ClassLoader classLoader, String className, String methodName, + List blockNodes) { + if (className == null || methodName == null || blockNodes == null) + throw new IllegalArgumentException("null given"); + + this.className = className; + this.methodName = methodName; + this.classLoader = classLoader; + + setId(); + setInstructions(blockNodes); + } + + /** + * Used by Entry- and ExitBlocks + * + * @param className a {@link java.lang.String} object. + * @param methodName a {@link java.lang.String} object. + */ + protected BasicBlock(String className, String methodName) { + if (className == null || methodName == null) + throw new IllegalArgumentException("null given"); + + this.className = className; + this.methodName = methodName; + this.isAuxiliaryBlock = true; + } + + // CDs + + /** + * Returns the ControlDependenceGraph of this instructions method + *

+ * Convenience method. Redirects the call to GraphPool.getCDG() + * + * @return a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cdg.ControlDependenceGraph} object. + */ + public ControlDependenceGraph getCDG() { + + ControlDependenceGraph myCDG = GraphPool.getInstance(classLoader).getCDG(className, + methodName); + if (myCDG == null) + throw new IllegalStateException( + "expect GraphPool to know CDG for every method for which an instruction is known"); + + return myCDG; + } + + /** + * Returns a cfg.Branch object for each branch this instruction is control + * dependent on as determined by the ControlDependenceGraph. If this + * instruction is only dependent on the root branch this method returns an + * empty set + *

+ * If this instruction is a Branch and it is dependent on itself - which can + * happen in loops for example - the returned set WILL contain this. If you + * do not need the full set in order to avoid loops, call + * getAllControlDependentBranches instead + * + * @return a {@link java.util.Set} object. + */ + public Set getControlDependencies() { + + if (controlDependencies == null) + controlDependencies = getCDG().getControlDependentBranches(this); + + // return new HashSet(controlDependentBranches); + return controlDependencies; + } + + /** + *

+ * hasControlDependenciesSet + *

+ * + * @return a boolean. + */ + public boolean hasControlDependenciesSet() { + return controlDependencies != null; + } + + // initialization + + private void setInstructions(List blockNodes) { + for (BytecodeInstruction instruction : blockNodes) { + if (!appendInstruction(instruction)) + throw new IllegalStateException( + "internal error while addind instruction to basic block list"); + } + if (instructions.isEmpty()) + throw new IllegalStateException( + "expect each basic block to contain at least one instruction"); + } + + private boolean appendInstruction(BytecodeInstruction instruction) { + if (instruction == null) + throw new IllegalArgumentException("null given"); + if (!className.equals(instruction.getClassName())) + throw new IllegalArgumentException( + "expect elements of a basic block to be inside the same class"); + if (!methodName.equals(instruction.getMethodName())) + throw new IllegalArgumentException( + "expect elements of a basic block to be inside the same class"); + if (instruction.hasBasicBlockSet()) + throw new IllegalArgumentException( + "expect to get instruction without BasicBlock already set"); + if (instructions.contains(instruction)) + throw new IllegalArgumentException( + "a basic block can not contain the same element twice"); + + instruction.setBasicBlock(this); + + return instructions.add(instruction); + } + + private void setId() { + blockCount++; + this.id = blockCount; + } + + // retrieve information + + /** + *

+ * containsInstruction + *

+ * + * @param instruction a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction} object. + * @return a boolean. + */ + public boolean containsInstruction(BytecodeInstruction instruction) { + if (instruction == null) + throw new IllegalArgumentException("null given"); + + return instructions.contains(instruction); + } + + /** + *

+ * getFirstInstruction + *

+ * + * @return a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction} object. + */ + public BytecodeInstruction getFirstInstruction() { + if (instructions.isEmpty()) + return null; + return instructions.get(0); + } + + /** + *

+ * getLastInstruction + *

+ * + * @return a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction} object. + */ + public BytecodeInstruction getLastInstruction() { + if (instructions.isEmpty()) + return null; + return instructions.get(instructions.size() - 1); + } + + /** + *

+ * getFirstLine + *

+ * + * @return a int. + */ + public int getFirstLine() { + for (BytecodeInstruction ins : instructions) + if (ins.hasLineNumberSet()) + return ins.getLineNumber(); + + return -1; + } + + /** + *

+ * getLastLine + *

+ * + * @return a int. + */ + public int getLastLine() { + + int r = -1; + + for (BytecodeInstruction ins : instructions) + if (ins.hasLineNumberSet()) + r = ins.getLineNumber(); + + return r; + } + + /** + *

+ * getName + *

+ * + * @return a {@link java.lang.String} object. + */ + public String getName() { + return (isAuxiliaryBlock ? "aux" : "") + "BasicBlock " + id; + // +" - "+methodName; + } + + /** + *

+ * Getter for the field className. + *

+ * + * @return a {@link java.lang.String} object. + */ + public String getClassName() { + return className; + } + + /** + *

+ * Getter for the field methodName. + *

+ * + * @return a {@link java.lang.String} object. + */ + public String getMethodName() { + return methodName; + } + + // inherited from Object + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + + String r = "BB" + id; + + if (instructions.size() < 5) + for (BytecodeInstruction ins : instructions) + r = r.trim() + " " + ins.getInstructionType(); + else + r += " " + getFirstInstruction().getInstructionType() + " ... " + + getLastInstruction().getInstructionType(); + + int startLine = getFirstLine(); + int endLine = getLastLine(); + r += " l" + (startLine == -1 ? "?" : startLine + ""); + r += "-l" + (endLine == -1 ? "?" : endLine + ""); + + return r; + } + + + // sanity check + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + ((className == null) ? 0 : className.hashCode()); + result = prime * result + id; + result = prime * result + + ((instructions == null) ? 0 : instructions.hashCode()); + result = prime * result + (isAuxiliaryBlock ? 1231 : 1237); + result = prime * result + + ((methodName == null) ? 0 : methodName.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!(obj instanceof BasicBlock)) + return false; + BasicBlock other = (BasicBlock) obj; + if (id != other.id) + return false; + if (className == null) { + if (other.className != null) + return false; + } else if (!className.equals(other.className)) + return false; + if (methodName == null) { + if (other.methodName != null) + return false; + } else if (!methodName.equals(other.methodName)) + return false; + if (instructions == null) { + if (other.instructions != null) + return false; + } else if (!instructions.equals(other.instructions)) + return false; + if (isEntryBlock() != other.isEntryBlock()) + return false; + return isExitBlock() == other.isExitBlock(); + } + + /** + *

+ * isEntryBlock + *

+ * + * @return a boolean. + */ + public boolean isEntryBlock() { + return false; + } + + /** + *

+ * isExitBlock + *

+ * + * @return a boolean. + */ + public boolean isExitBlock() { + return false; + } + + /* (non-Javadoc) + * @see java.lang.Iterable#iterator() + */ + + /** + * {@inheritDoc} + */ + @Override + public Iterator iterator() { + return instructions.iterator(); + } +} diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/BytecodeInstruction.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/BytecodeInstruction.java new file mode 100755 index 0000000000..43cb8ff21b --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/BytecodeInstruction.java @@ -0,0 +1,778 @@ +/* + * Adapted from the EvoSuite project (https://github.com/EvoSuite/evosuite) + * and modified for use in EvoMaster's Dynamosa module. + */ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg; + +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.branch.Branch; +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.branch.BranchPool; + +import org.evomaster.client.java.instrumentation.dynamosa.graphs.GraphPool; +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cdg.ControlDependenceGraph; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.*; +import org.objectweb.asm.util.Printer; + +import java.io.Serializable; +import java.util.Set; + +/** + * Internal representation of a BytecodeInstruction + */ +public class BytecodeInstruction implements Serializable, + Comparable { + + private static final long serialVersionUID = 3630449183355518857L; + + // from ASM library + protected AbstractInsnNode asmNode; + + // identification of a byteCode instruction inside EvoSuite + protected ClassLoader classLoader; + protected String className; + protected String methodName; + protected int instructionId; + protected int bytecodeOffset; + + // auxiliary information + private int lineNumber = -1; + + // experiment: also searching through all CFG nodes in order to determine an + // instruction BasicBlock might be a little to expensive too just to safe + // space for one reference + private BasicBlock basicBlock; + + /** + * Generates a ByteCodeInstruction instance that represents a byteCode + * instruction as indicated by the given ASMNode in the given method and + * class + * + * @param className a {@link java.lang.String} object. + * @param methodName a {@link java.lang.String} object. + * @param instructionId a int. + * @param bytecodeOffset a int. + * @param asmNode a {@link org.objectweb.asm.tree.AbstractInsnNode} object. + */ + public BytecodeInstruction(ClassLoader classLoader, String className, + String methodName, int instructionId, int bytecodeOffset, AbstractInsnNode asmNode) { + + if (className == null || methodName == null || asmNode == null) + throw new IllegalArgumentException("null given"); + if (instructionId < 0) + throw new IllegalArgumentException( + "expect instructionId to be positive, not " + instructionId); + + this.instructionId = instructionId; + this.bytecodeOffset = bytecodeOffset; + this.asmNode = asmNode; + + this.classLoader = classLoader; + + setClassName(className); + setMethodName(methodName); + } + + /** + * Can represent any byteCode instruction + * + * @param wrap a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction} object. + */ + public BytecodeInstruction(BytecodeInstruction wrap) { + + this(wrap.classLoader, wrap.className, wrap.methodName, wrap.instructionId, + wrap.bytecodeOffset, wrap.asmNode, wrap.lineNumber, wrap.basicBlock); + } + + /** + *

+ * Constructor for BytecodeInstruction. + *

+ * + * @param className a {@link java.lang.String} object. + * @param methodName a {@link java.lang.String} object. + * @param instructionId a int. + * @param bytecodeOffset a int. + * @param asmNode a {@link org.objectweb.asm.tree.AbstractInsnNode} object. + * @param lineNumber a int. + * @param basicBlock a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BasicBlock} object. + */ + public BytecodeInstruction(ClassLoader classLoader, String className, + String methodName, int instructionId, int bytecodeOffset, AbstractInsnNode asmNode, + int lineNumber, BasicBlock basicBlock) { + + this(classLoader, className, methodName, instructionId, bytecodeOffset, asmNode, + lineNumber); + + this.basicBlock = basicBlock; + } + + /** + *

+ * Constructor for BytecodeInstruction. + *

+ * + * @param className a {@link java.lang.String} object. + * @param methodName a {@link java.lang.String} object. + * @param instructionId a int. + * @param bytecodeOffset a int. + * @param asmNode a {@link org.objectweb.asm.tree.AbstractInsnNode} object. + * @param lineNumber a int. + */ + public BytecodeInstruction(ClassLoader classLoader, String className, + String methodName, int instructionId, int bytecodeOffset, AbstractInsnNode asmNode, + int lineNumber) { + + this(classLoader, className, methodName, instructionId, bytecodeOffset, asmNode); + + if (lineNumber != -1) + setLineNumber(lineNumber); + } + + // getter + setter + + private void setMethodName(String methodName) { + if (methodName == null) + throw new IllegalArgumentException("null given"); + + this.methodName = methodName; + } + + private void setClassName(String className) { + if (className == null) + throw new IllegalArgumentException("null given"); + + this.className = className; + } + + // --- Field Management --- + + /** + * {@inheritDoc} + */ + public int getInstructionId() { + return instructionId; + } + + /** + *

+ * getBytecodeOffset + *

+ * + * @return a int. + */ + public int getBytecodeOffset() { + return bytecodeOffset; + } + + /** + * {@inheritDoc} + */ + public String getMethodName() { + return methodName; + } + + /** + *

+ * Getter for the field className. + *

+ * + * @return a {@link java.lang.String} object. + */ + public String getClassName() { + return className; + } + + /** + * Return's the BasicBlock that contain's this instruction in it's CFG. + *

+ * If no BasicBlock containing this instruction was created yet, null is + * returned. + * + * @return a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BasicBlock} object. + */ + public BasicBlock getBasicBlock() { + if (!hasBasicBlockSet()) + retrieveBasicBlock(); + return basicBlock; + } + + private void retrieveBasicBlock() { + + if (basicBlock == null) + basicBlock = getActualCFG().getBlockOf(this); + } + + /** + * Once the CFG has been asked for this instruction's BasicBlock it sets + * this instance's internal basicBlock field. + * + * @param block a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BasicBlock} object. + */ + public void setBasicBlock(BasicBlock block) { + if (block == null) + throw new IllegalArgumentException("null given"); + + if (!block.getClassName().equals(getClassName()) + || !block.getMethodName().equals(getMethodName())) + throw new IllegalArgumentException( + "expect block to be for the same method and class as this instruction"); + if (this.basicBlock != null) + throw new IllegalArgumentException( + "basicBlock already set! not allowed to overwrite"); + + this.basicBlock = block; + } + + /** + * Checks whether this instance's basicBlock has already been set by the CFG + * or + * + * @return a boolean. + */ + public boolean hasBasicBlockSet() { + return basicBlock != null; + } + + /** + * {@inheritDoc} + */ + public int getLineNumber() { + + if (lineNumber == -1 && isLineNumber()) + retrieveLineNumber(); + + return lineNumber; + } + + /** + *

+ * Setter for the field lineNumber. + *

+ * + * @param lineNumber a int. + */ + public void setLineNumber(int lineNumber) { + if (lineNumber <= 0) + throw new IllegalArgumentException( + "expect lineNumber value to be positive"); + + if (isLabel()) + return; + + if (isLineNumber()) { + int asmLine = getASMLineNumber(); + // sanity check + if (lineNumber != -1 && asmLine != lineNumber) + throw new IllegalStateException( + "linenumber instruction has lineNumber field set to a value different from instruction linenumber"); + this.lineNumber = asmLine; + } else { + this.lineNumber = lineNumber; + } + } + + /** + * At first, if this instruction constitutes a line number instruction this + * method tries to retrieve the lineNumber from the underlying asmNode and + * set the lineNumber field to the value given by the asmNode. + *

+ * This can lead to an IllegalStateException, should the lineNumber field + * have been set to another value previously + *

+ * After that, if the lineNumber field is still not initialized, this method + * returns false Otherwise it returns true + * + * @return a boolean. + */ + public boolean hasLineNumberSet() { + retrieveLineNumber(); + return lineNumber != -1; + } + + /** + * If the underlying ASMNode is a LineNumberNode the lineNumber field of + * this instance will be set to the lineNumber contained in that + * LineNumberNode + *

+ * Should the lineNumber field have been set to a value different from that + * contained in the asmNode, this method throws an IllegalStateExeption + */ + private void retrieveLineNumber() { + if (isLineNumber()) { + int asmLine = getASMLineNumber(); + // sanity check + if (this.lineNumber != -1 && asmLine != this.lineNumber) + throw new IllegalStateException( + "lineNumber field was manually set to a value different from the actual lineNumber contained in LineNumberNode"); + this.lineNumber = asmLine; + } + } + + // --- graph section --- + + /** + * Returns the ActualControlFlowGraph of this instructions method + *

+ * Convenience method. Redirects the call to GraphPool.getActualCFG() + * + * @return a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.ActualControlFlowGraph} object. + */ + public ActualControlFlowGraph getActualCFG() { + + ActualControlFlowGraph myCFG = GraphPool.getInstance(classLoader).getActualCFG(className, + methodName); + if (myCFG == null) + throw new IllegalStateException( + "expect GraphPool to know CFG for every method for which an instruction is known"); + + return myCFG; + } + + /** + * Returns the RawControlFlowGraph of this instructions method + *

+ * Convenience method. Redirects the call to GraphPool.getRawCFG() + * + * @return a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.RawControlFlowGraph} object. + */ + public RawControlFlowGraph getRawCFG() { + + RawControlFlowGraph myCFG = GraphPool.getInstance(classLoader).getRawCFG(className, + methodName); + if (myCFG == null) + throw new IllegalStateException( + "expect GraphPool to know CFG for every method for which an instruction is known"); + + return myCFG; + } + + /** + * Returns the ControlDependenceGraph of this instructions method + *

+ * Convenience method. Redirects the call to GraphPool.getCDG() + * + * @return a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cdg.ControlDependenceGraph} object. + */ + public ControlDependenceGraph getCDG() { + + ControlDependenceGraph myCDG = GraphPool.getInstance(classLoader).getCDG(className, + methodName); + if (myCDG == null) + throw new IllegalStateException( + "expect GraphPool to know CDG for every method for which an instruction is known"); + + return myCDG; + } + + // --- CDG-Section --- + + /** + * Returns a cfg.Branch object for each branch this instruction is control + * dependent on as determined by the ControlDependenceGraph. If this + * instruction is only dependent on the root branch this method returns an + * empty set + *

+ * If this instruction is a Branch and it is dependent on itself - which can + * happen in loops for example - the returned set WILL contain this. If you + * do not need the full set in order to avoid loops, call + * getAllControlDependentBranches instead + * + * @return a {@link java.util.Set} object. + */ + public Set getControlDependencies() { + + BasicBlock myBlock = getBasicBlock(); + + // return new + // HashSet(myBlock.getControlDependencies()); + return myBlock.getControlDependencies(); + } + + public Branch getControlDependentBranch() { + + Set controlDependentBranches = getControlDependencies(); + + for (ControlDependency cd : controlDependentBranches) + return cd.getBranch(); + + return null; // root branch + } + + // String methods + + /** + *

+ * explain + *

+ * + * @return a {@link java.lang.String} object. + */ + public String explain() { + if (isBranch()) { + if (BranchPool.getInstance(classLoader).isKnownAsBranch(this)) { + Branch b = BranchPool.getInstance(classLoader).getBranchForInstruction(this); + if (b == null) + throw new IllegalStateException( + "expect BranchPool to be able to return Branches for instructions fullfilling BranchPool.isKnownAsBranch()"); + + return "Branch " + b.getActualBranchId() + " - " + + getInstructionType(); + } + return "UNKNOWN Branch I" + instructionId + " " + + getInstructionType() + ", jump to " + ((JumpInsnNode) asmNode).label.getLabel(); + + // + " - " + ((JumpInsnNode) asmNode).label.getLabel(); + } + + return getASMNodeString(); + } + + /** + *

+ * getASMNodeString + *

+ * + * @return a {@link java.lang.String} object. + */ + public String getASMNodeString() { + String type = getType(); + String opcode = getInstructionType(); + + String stack = "n/a"; + + if (asmNode instanceof LabelNode) { + return "LABEL " + ((LabelNode) asmNode).getLabel().toString(); + } else if (asmNode instanceof FieldInsnNode) + return "Field" + " " + ((FieldInsnNode) asmNode).owner + "." + + ((FieldInsnNode) asmNode).name + " Type=" + type + + ", Opcode=" + opcode; + else if (asmNode instanceof FrameNode) + return "Frame" + " " + asmNode.getOpcode() + " Type=" + type + + ", Opcode=" + opcode; + else if (asmNode instanceof IincInsnNode) + return "IINC " + ((IincInsnNode) asmNode).var + " Type=" + type + + ", Opcode=" + opcode; + else if (asmNode instanceof InsnNode) + return "" + opcode; + else if (asmNode instanceof IntInsnNode) + return "INT " + ((IntInsnNode) asmNode).operand + " Type=" + type + + ", Opcode=" + opcode; + else if (asmNode instanceof MethodInsnNode) + return opcode + " " + ((MethodInsnNode) asmNode).owner + "." + ((MethodInsnNode) asmNode).name + ((MethodInsnNode) asmNode).desc; + else if (asmNode instanceof JumpInsnNode) + return "JUMP " + ((JumpInsnNode) asmNode).label.getLabel() + + " Type=" + type + ", Opcode=" + opcode + ", Stack: " + + stack + " - Line: " + lineNumber; + else if (asmNode instanceof LdcInsnNode) + return "LDC " + ((LdcInsnNode) asmNode).cst + " Type=" + type; // + + // ", Opcode="; + // + opcode; // cst starts with mutationid if + // this is location of mutation + else if (asmNode instanceof LineNumberNode) + return "LINE " + " " + ((LineNumberNode) asmNode).line; + else if (asmNode instanceof LookupSwitchInsnNode) + return "LookupSwitchInsnNode" + " " + asmNode.getOpcode() + + " Type=" + type + ", Opcode=" + opcode; + else if (asmNode instanceof MultiANewArrayInsnNode) + return "MULTIANEWARRAY " + " " + asmNode.getOpcode() + " Type=" + + type + ", Opcode=" + opcode; + else if (asmNode instanceof TableSwitchInsnNode) + return "TableSwitchInsnNode" + " " + asmNode.getOpcode() + " Type=" + + type + ", Opcode=" + opcode; + else if (asmNode instanceof TypeInsnNode) { + switch (asmNode.getOpcode()) { + case Opcodes.NEW: + return "NEW " + ((TypeInsnNode) asmNode).desc; + case Opcodes.ANEWARRAY: + return "ANEWARRAY " + ((TypeInsnNode) asmNode).desc; + case Opcodes.CHECKCAST: + return "CHECKCAST " + ((TypeInsnNode) asmNode).desc; + case Opcodes.INSTANCEOF: + return "INSTANCEOF " + ((TypeInsnNode) asmNode).desc; + default: + return "Unknown node" + " Type=" + type + ", Opcode=" + opcode; + } + } + // return "TYPE " + " " + node.getOpcode() + " Type=" + type + // + ", Opcode=" + opcode; + else if (asmNode instanceof VarInsnNode) + return opcode + " " + ((VarInsnNode) asmNode).var; + else + return "Unknown node" + " Type=" + type + ", Opcode=" + opcode; + } + + // --- Inherited from Object --- + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + + String r = "I" + instructionId; + + r += " (" + +bytecodeOffset + ")"; + r += " " + explain(); + + if (hasLineNumberSet() && !isLineNumber()) + r += " l" + getLineNumber(); + + return r; + } + + /** + * Convenience method: + *

+ * If this instruction is known by the BranchPool to be a Branch, you can + * call this method in order to retrieve the corresponding Branch object + * registered within the BranchPool. + *

+ * Otherwise this method will return null; + * + * @return a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.branch.Branch} object. + */ + public Branch toBranch() { + + try { + return BranchPool.getInstance(classLoader).getBranchForInstruction(this); + } catch (Exception e) { + return null; + } + } + + /** + *

+ * isLastInstructionInMethod + *

+ * + * @return a boolean. + */ + public boolean isLastInstructionInMethod() { + return equals(getRawCFG().getInstructionWithBiggestId()); + } + + /** + *

+ * canBeExitPoint + *

+ * + * @return a boolean. + */ + public boolean canBeExitPoint() { + return canReturnFromMethod() || isLastInstructionInMethod(); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + ((className == null) ? 0 : className.hashCode()); + result = prime * result + instructionId; + result = prime * result + + ((methodName == null) ? 0 : methodName.hashCode()); + return result; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + BytecodeInstruction other = (BytecodeInstruction) obj; + if (className == null) { + if (other.className != null) + return false; + } else if (!className.equals(other.className)) + return false; + if (instructionId != other.instructionId) + return false; + if (methodName == null) { + return other.methodName == null; + } else return methodName.equals(other.methodName); + } + + // inherited from Object + + /* + * (non-Javadoc) + * + * @see java.lang.Comparable#compareTo(java.lang.Object) + */ + @Override + public int compareTo(BytecodeInstruction o) { + return getLineNumber() - o.getLineNumber(); + } + + /** + *

+ * getASMNode + *

+ * + * @return a {@link org.objectweb.asm.tree.AbstractInsnNode} object. + */ + public AbstractInsnNode getASMNode() { + return asmNode; + } + + /** + *

+ * getInstructionType + *

+ * + * @return a {@link java.lang.String} object. + */ + public String getInstructionType() { + + if (asmNode.getOpcode() >= 0 && asmNode.getOpcode() < Printer.OPCODES.length) + return Printer.OPCODES[asmNode.getOpcode()]; + + if (isLineNumber()) + return "LINE " + this.getLineNumber(); + + return getType(); + } + + /** + *

+ * getType + *

+ * + * @return a {@link java.lang.String} object. + */ + public String getType() { + // TODO explain + String type = ""; + if (asmNode.getType() >= 0 && asmNode.getType() < Printer.TYPES.length) + type = Printer.TYPES[asmNode.getType()]; + + return type; + } + + /** + *

+ * canReturnFromMethod + *

+ * + * @return a boolean. + */ + public boolean canReturnFromMethod() { + return isReturn() || isThrow(); + } + + /** + *

+ * isReturn + *

+ * + * @return a boolean. + */ + public boolean isReturn() { + switch (asmNode.getOpcode()) { + case Opcodes.RETURN: + case Opcodes.ARETURN: + case Opcodes.IRETURN: + case Opcodes.LRETURN: + case Opcodes.DRETURN: + case Opcodes.FRETURN: + return true; + default: + return false; + } + } + + /** + *

+ * isThrow + *

+ * + * @return a boolean. + */ + public boolean isThrow() { + // TODO: Need to check if this is a caught exception? + return asmNode.getOpcode() == Opcodes.ATHROW; + } + + /** + *

+ * isJump + *

+ * + * @return a boolean. + */ + public boolean isJump() { + return (asmNode instanceof JumpInsnNode); + } + + /** + *

+ * isGoto + *

+ * + * @return a boolean. + */ + public boolean isGoto() { + if (asmNode instanceof JumpInsnNode) { + return (asmNode.getOpcode() == Opcodes.GOTO); + } + return false; + } + + /** + *

+ * isBranch + *

+ * + * @return a boolean. + */ + public boolean isBranch() { + return (isJump() && !isGoto()); + } + + /** + * Determines if this instruction is a line number instruction + *

+ * More precisely this method checks if the underlying asmNode is a + * LineNumberNode + * + * @return a boolean. + */ + public boolean isLineNumber() { + return (asmNode instanceof LineNumberNode); + } + + /** + *

+ * getASMLineNumber + *

+ * + * @return a int. + */ + public int getASMLineNumber() { + if (!isLineNumber()) + return -1; + + return ((LineNumberNode) asmNode).line; + } + + /** + *

+ * isLabel + *

+ * + * @return a boolean. + */ + public boolean isLabel() { + return asmNode instanceof LabelNode; + } + +} diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/BytecodeInstructionPool.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/BytecodeInstructionPool.java new file mode 100755 index 0000000000..2026f747ed --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/BytecodeInstructionPool.java @@ -0,0 +1,379 @@ +/* + * Adapted from the EvoSuite project (https://github.com/EvoSuite/evosuite) + * and modified for use in EvoMaster's Dynamosa module. + */ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg; + +import org.evomaster.client.java.instrumentation.dynamosa.AnnotatedLabel; +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.branch.BranchPool; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.*; +import org.evomaster.client.java.utils.SimpleLogger; + +import java.util.*; + +/** + *

+ * BytecodeInstructionPool class. + *

+ * + */ +public class BytecodeInstructionPool { + + private static final Map instanceMap = new LinkedHashMap<>(); + + private final ClassLoader classLoader; + + private BytecodeInstructionPool(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + public static BytecodeInstructionPool getInstance(ClassLoader classLoader) { + if (!instanceMap.containsKey(classLoader)) { + instanceMap.put(classLoader, new BytecodeInstructionPool(classLoader)); + } + + return instanceMap.get(classLoader); + } + + // maps className -> method inside that class -> list of + // BytecodeInstructions + private final Map>> instructionMap = new LinkedHashMap<>(); + + // fill the pool + + /** + * Called by each CFGGenerator for it's corresponding method. + *

+ * The MethodNode contains all instructions within a method. A call to + * registerMethodNode() fills the instructionMap of the + * BytecodeInstructionPool with the instructions in that method and returns + * a List containing the BytecodeInstructions within that method. + *

+ * While registering all instructions the lineNumber of each + * BytecodeInstruction is set. + * + * @param node a {@link org.objectweb.asm.tree.MethodNode} object. + * @param className a {@link java.lang.String} object. + * @param methodName a {@link java.lang.String} object. + * @return a {@link java.util.List} object. + */ + public List registerMethodNode(MethodNode node, + String className, String methodName) { + int lastLineNumber = -1; + int bytecodeOffset = 0; + + for (int instructionId = 0; instructionId < node.instructions.size(); instructionId++) { + AbstractInsnNode instructionNode = node.instructions.get(instructionId); + + BytecodeInstruction instruction = new BytecodeInstruction( + classLoader, + className, + methodName, + instructionId, + bytecodeOffset, + instructionNode + ); + + if (instruction.isLineNumber()) + lastLineNumber = instruction.getLineNumber(); + else if (lastLineNumber != -1) + instruction.setLineNumber(lastLineNumber); + + bytecodeOffset += getBytecodeIncrement(instructionNode); + + if (!instruction.isLabel() && !instruction.isLineNumber() + && !(instruction.getASMNode() instanceof FrameNode)) { + bytecodeOffset++; + } + + registerInstruction(instruction); + + } + + List r = getInstructionsIn(className, methodName); + if (r == null || r.size() == 0) + throw new IllegalStateException( + "expect instruction pool to return non-null non-empty list of instructions for a previously registered method " + + methodName); + + return r; + } + + /** + * Determine how many bytes the current instruction occupies together with + * its operands + * + * @return + */ + private int getBytecodeIncrement(AbstractInsnNode instructionNode) { + int opcode = instructionNode.getOpcode(); + switch (opcode) { + case Opcodes.ALOAD: // index + case Opcodes.ASTORE: // index + case Opcodes.DLOAD: + case Opcodes.DSTORE: + case Opcodes.FLOAD: + case Opcodes.FSTORE: + case Opcodes.ILOAD: + case Opcodes.ISTORE: + case Opcodes.LLOAD: + case Opcodes.LSTORE: + VarInsnNode varNode = (VarInsnNode) instructionNode; + if (varNode.var > 3) + return 1; + else + return 0; + case Opcodes.BIPUSH: // byte + case Opcodes.NEWARRAY: + case Opcodes.RET: + return 1; + case Opcodes.LDC: + LdcInsnNode ldcNode = (LdcInsnNode) instructionNode; + if (ldcNode.cst instanceof Double || ldcNode.cst instanceof Long) + return 2; // LDC2_W + else + return 1; + case 19: //LDC_W + case 20: //LDC2_W + return 2; + case Opcodes.ANEWARRAY: // indexbyte1, indexbyte2 + case Opcodes.CHECKCAST: // indexbyte1, indexbyte2 + case Opcodes.GETFIELD: + case Opcodes.GETSTATIC: + case Opcodes.GOTO: + case Opcodes.IF_ACMPEQ: + case Opcodes.IF_ACMPNE: + case Opcodes.IF_ICMPEQ: + case Opcodes.IF_ICMPNE: + case Opcodes.IF_ICMPGE: + case Opcodes.IF_ICMPGT: + case Opcodes.IF_ICMPLE: + case Opcodes.IF_ICMPLT: + case Opcodes.IFLE: + case Opcodes.IFLT: + case Opcodes.IFGE: + case Opcodes.IFGT: + case Opcodes.IFNE: + case Opcodes.IFEQ: + case Opcodes.IFNONNULL: + case Opcodes.IFNULL: + case Opcodes.IINC: + case Opcodes.INSTANCEOF: + case Opcodes.INVOKESPECIAL: + case Opcodes.INVOKESTATIC: + case Opcodes.INVOKEVIRTUAL: + case Opcodes.JSR: + case Opcodes.NEW: + case Opcodes.PUTFIELD: + case Opcodes.PUTSTATIC: + case Opcodes.SIPUSH: + // case Opcodes.LDC_W + // case Opcodes.LDC2_W + + return 2; + case Opcodes.MULTIANEWARRAY: + return 3; + case Opcodes.INVOKEDYNAMIC: + case Opcodes.INVOKEINTERFACE: + return 4; + + case Opcodes.LOOKUPSWITCH: + case Opcodes.TABLESWITCH: + // TODO: Could be more + return 4; + // case Opcodes.GOTO_W + // case Opcodes.JSR_W + } + return 0; + } + + /** + *

+ * registerInstruction + *

+ * + * @param instruction a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction} object. + */ + public void registerInstruction(BytecodeInstruction instruction) { + String className = instruction.getClassName(); + String methodName = instruction.getMethodName(); + + if (!instructionMap.containsKey(className)) + instructionMap.put(className, + new LinkedHashMap<>()); + if (!instructionMap.get(className).containsKey(methodName)) + instructionMap.get(className).put(methodName, + new ArrayList<>()); + + instructionMap.get(className).get(methodName).add(instruction); + SimpleLogger.debug("Registering instruction " + instruction); + List instructions = instructionMap.get(className).get(methodName); + if (instructions.size() > 1) { + BytecodeInstruction previous = instructions.get(instructions.size() - 2); + if (previous.isLabel()) { + LabelNode ln = (LabelNode) previous.asmNode; + if (ln.getLabel() instanceof AnnotatedLabel) { + AnnotatedLabel aLabel = (AnnotatedLabel) ln.getLabel(); + if (aLabel.isStartTag()) { + if (aLabel.shouldIgnore()) { + SimpleLogger.debug("Ignoring artificial branch: " + instruction); + return; + } + } + } + } + } + + if (instruction.isBranch()) { + BranchPool.getInstance(classLoader).registerAsBranch(instruction); + } + } + + // retrieve data from the pool + + /** + *

+ * getInstruction + *

+ * + * @param className a {@link java.lang.String} object. + * @param methodName a {@link java.lang.String} object. + * @param instructionId a int. + * @param asmNode a {@link org.objectweb.asm.tree.AbstractInsnNode} object. + * @return a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction} object. + */ + public BytecodeInstruction getInstruction(String className, String methodName, + int instructionId, AbstractInsnNode asmNode) { + + BytecodeInstruction r = getInstruction(className, methodName, instructionId); + + assert r == null || (asmNode != null && asmNode.equals(r.getASMNode())); + + return r; + } + + /** + *

+ * getInstruction + *

+ * + * @param className a {@link java.lang.String} object. + * @param methodName a {@link java.lang.String} object. + * @param instructionId a int. + * @return a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction} object. + */ + public BytecodeInstruction getInstruction(String className, String methodName, + int instructionId) { + + if (instructionMap.get(className) == null) { + SimpleLogger.debug("unknown class: " + className); + SimpleLogger.debug(instructionMap.keySet().toString()); + return null; + } + if (instructionMap.get(className).get(methodName) == null) { + SimpleLogger.debug("unknown method: " + methodName); + SimpleLogger.debug(instructionMap.get(className).keySet().toString()); + return null; + } + for (BytecodeInstruction instruction : instructionMap.get(className).get(methodName)) { + if (instruction.getInstructionId() == instructionId) + return instruction; + } + + SimpleLogger.debug("unknown instruction " + instructionId + ", have " + + instructionMap.get(className).get(methodName).size()); + for (int i = 0; i < instructionMap.get(className).get(methodName).size(); i++) { + SimpleLogger.info(instructionMap.get(className).get(methodName).get(i).toString()); + } + + return null; + } + + /** + *

+ * getInstruction + *

+ * + * @param className a {@link java.lang.String} object. + * @param methodName a {@link java.lang.String} object. + * @param node a {@link org.objectweb.asm.tree.AbstractInsnNode} object. + * @return a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction} object. + */ + public BytecodeInstruction getInstruction(String className, String methodName, + AbstractInsnNode node) { + + if (instructionMap.get(className) == null) { + SimpleLogger.debug("unknown class: " + className); + SimpleLogger.debug(instructionMap.keySet().toString()); + return null; + } + if (instructionMap.get(className).get(methodName) == null) { + SimpleLogger.debug("unknown method: " + methodName); + SimpleLogger.debug(instructionMap.get(className).keySet().toString()); + return null; + } + for (BytecodeInstruction instruction : instructionMap.get(className).get(methodName)) { + if (instruction.asmNode == node) + return instruction; + } + + SimpleLogger.debug("unknown instruction: " + node + ", have " + + instructionMap.get(className).get(methodName).size() + + " instructions for this method"); + SimpleLogger.debug(instructionMap.get(className).get(methodName).toString()); + + return null; + } + + /** + *

+ * getInstructionsIn + *

+ * + * @param className a {@link java.lang.String} object. + * @param methodName a {@link java.lang.String} object. + * @return a {@link java.util.List} object. + */ + public List getInstructionsIn(String className, String methodName) { + if (instructionMap.get(className) == null + || instructionMap.get(className).get(methodName) == null) + return null; + + List r = new ArrayList<>(instructionMap.get(className).get(methodName)); + + return r; + } + + public List getInstructionsIn(String className) { + if (instructionMap.get(className) == null) + return null; + + List r = new ArrayList<>(); + Map> methodMap = instructionMap.get(className); + for (List methodInstructions : methodMap.values()) { + r.addAll(methodInstructions); + } + + return r; + } + + /** + *

+ * forgetInstruction + *

+ * + * @param ins a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction} object. + * @return a boolean. + */ + public boolean forgetInstruction(BytecodeInstruction ins) { + if (!instructionMap.containsKey(ins.getClassName())) + return false; + if (!instructionMap.get(ins.getClassName()).containsKey(ins.getMethodName())) + return false; + + return instructionMap.get(ins.getClassName()).get(ins.getMethodName()).remove(ins); + } + +} + diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/CFGGenerator.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/CFGGenerator.java new file mode 100755 index 0000000000..b541ba60e7 --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/CFGGenerator.java @@ -0,0 +1,225 @@ +/* + * Adapted from the EvoSuite project (https://github.com/EvoSuite/evosuite) + * and modified for use in EvoMaster's Dynamosa module. + */ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg; + +import org.evomaster.client.java.instrumentation.dynamosa.graphs.GraphPool; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.analysis.Frame; +import org.evomaster.client.java.utils.SimpleLogger; + +import java.util.List; + +/** + * This classed is used to create the RawControlFlowGraph which can then be used + * to create the ActualControlFlowGraph + *

+ * When analyzing a CUT the BytecodeAnalyzer creates an instance of this class + * for each method contained in it + *

+ * This class's methods get called in the following order: + *

+ * - upon constructing, the method at hand is registered via + * registerMethodNode() which fills the BytecodeInstructionPool with all + * instructions inside that method + *

+ * - then registerControlFlowEdge() is called by the BytecodeAnalyzer for each + * possible transition from one byteCode instruction to another within the + * current method. In this step the CFGGenerator asks the + * BytecodeInstructionPool for the previously created instructions and fills up + * it's RawControlFlowGraph + *

+ * After those calls the RawControlFlowGraph of the method at hand is complete + * It should contain a Vertex for each BytecodeInstruction inside the specified + * method and an edge for every possible transition between these instructions + * + */ +public class CFGGenerator { + + private RawControlFlowGraph rawGraph; + + private boolean nodeRegistered = false; + private MethodNode currentMethod; + private String className; + private String methodName; + private final ClassLoader classLoader; + + /** + * Initializes this generator to generate the CFG for the method identified + * by the given parameters + *

+ * Calls registerMethodNode() which in turn calls + * BytecodeInstructionPool.registerMethodNode() leading to the creation of + * all BytecodeInstruction instances for the method at hand + *

+ * TODO might not want to give asm.MethodNode to the outside, but rather a + * MyMethodNode extended from BytecodeInstruction or something + * + * @param className a {@link java.lang.String} object. + * @param methodName a {@link java.lang.String} object. + * @param node a {@link org.objectweb.asm.tree.MethodNode} object. + */ + public CFGGenerator(ClassLoader classLoader, String className, String methodName, + MethodNode node) { + this.classLoader = classLoader; + registerMethodNode(node, className, methodName); + } + + /** + * Adds the RawControlFlowGraph created by this instance to the GraphPool, + * computes the resulting ActualControlFlowGraph + */ + public void registerCFGs() { + + int removed = getRawGraph().removeIsolatedNodes(); + if (removed > 0) + SimpleLogger.info("removed isolated nodes: " + removed + " in " + methodName); + + // non-minimized cfg needed for defuse-coverage and control + // dependence calculation + GraphPool.getInstance(classLoader).registerRawCFG(getRawGraph()); + GraphPool.getInstance(classLoader).registerActualCFG(computeActualCFG()); + } + + // build up the graph + + private void registerMethodNode(MethodNode currentMethod, String className, + String methodName) { + if (nodeRegistered) + throw new IllegalStateException( + "registerMethodNode must not be called more than once for each instance of CFGGenerator"); + if (currentMethod == null || methodName == null || className == null) + throw new IllegalArgumentException("null given"); + + this.currentMethod = currentMethod; + this.className = className; + this.methodName = methodName; + + this.rawGraph = new RawControlFlowGraph(classLoader, className, methodName, + currentMethod.access); + + List instructionsInMethod = BytecodeInstructionPool.getInstance(classLoader).registerMethodNode(currentMethod, + className, + methodName); + + // sometimes there is a Label at the very end of a method without a + // controlFlowEdge to it. In order to keep the graph as connected as + // possible and since this is just a label we will simply ignore these + int count = 0; + for (BytecodeInstruction ins : instructionsInMethod) { + count++; + if (!ins.isLabel() || count < instructionsInMethod.size()) + rawGraph.addVertex(ins); + } + + nodeRegistered = true; + } + + /** + * Internal management of fields and actual building up of the rawGraph + *

+ * Is called by the corresponding BytecodeAnalyzer whenever it detects a + * control flow edge + * + * @param src a int. + * @param dst a int. + * @param frames an array of {@link org.objectweb.asm.tree.analysis.Frame} + * objects. + * @param isExceptionEdge a boolean. + */ + public void registerControlFlowEdge(int src, int dst, Frame[] frames, + boolean isExceptionEdge) { + if (!nodeRegistered) + throw new IllegalStateException( + "CFGGenrator.registerControlFlowEdge() cannot be called unless registerMethodNode() was called first"); + if (frames == null) + throw new IllegalArgumentException("null given"); + Frame dstFrame = frames[dst]; + + if (dstFrame == null) { + // documentation of getFrames() tells us the following: + // Returns: + // the symbolic state of the execution stack frame at each bytecode + // instruction of the method. The size of the returned array is + // equal to the number of instructions (and labels) of the method. A + // given frame is null if the corresponding instruction cannot be + // reached, or if an error occured during the analysis of the + // method. + // so let's say we expect the analyzer to return null only if + // dst is not reachable and if that happens we just suppress the + // corresponding ControlFlowEdge for now + // TODO can the CFG become disconnected like that? + return; + } + + AbstractInsnNode srcNode = currentMethod.instructions.get(src); + AbstractInsnNode dstNode = currentMethod.instructions.get(dst); + + // those nodes should have gotten registered by registerMethodNode() + BytecodeInstruction srcInstruction = BytecodeInstructionPool.getInstance(classLoader).getInstruction(className, + methodName, + src, + srcNode); + BytecodeInstruction dstInstruction = BytecodeInstructionPool.getInstance(classLoader).getInstruction(className, + methodName, + dst, + dstNode); + + if (dstInstruction == null) + throw new IllegalStateException( + "expect BytecodeInstructionPool to know the instructions in the method of this edge"); + + if (null == rawGraph.addEdge(srcInstruction, dstInstruction, isExceptionEdge)) + SimpleLogger.error("internal error while adding edge"); + } + + /** + * Computes the ActualCFG with BasicBlocks rather then BytecodeInstructions + * for this RawCFG. + *

+ * See ActualControlFlowGraph and GraphPool for further details. + * + * @return a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.ActualControlFlowGraph} object. + */ + public ActualControlFlowGraph computeActualCFG() { + return new ActualControlFlowGraph(rawGraph); + } + + // getter + + /** + *

+ * Getter for the field rawGraph. + *

+ * + * @return a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.RawControlFlowGraph} object. + */ + public RawControlFlowGraph getRawGraph() { + return rawGraph; + } + + /** + *

+ * Getter for the field className. + *

+ * + * @return a {@link java.lang.String} object. + */ + public String getClassName() { + return className; + } + + /** + *

+ * Getter for the field methodName. + *

+ * + * @return a {@link java.lang.String} object. + */ + public String getMethodName() { + return methodName; + } + +} diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/ControlDependency.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/ControlDependency.java new file mode 100755 index 0000000000..7a44ead7be --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/ControlDependency.java @@ -0,0 +1,79 @@ +/* + * Adapted from the EvoSuite project (https://github.com/EvoSuite/evosuite) + * and modified for use in EvoMaster's Dynamosa module. + */ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg; + +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.branch.Branch; + +import java.util.Objects; + +public class ControlDependency { + + private final Branch branch; + private final boolean branchExpressionValue; + + /** + *

Constructor for ControlDependency.

+ * + * @param branch a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.branch.Branch} object. + * @param branchExpressionValue a boolean. + */ + public ControlDependency(Branch branch, boolean branchExpressionValue) { + if (branch == null) + throw new IllegalArgumentException( + "control dependencies for the root branch are not permitted (null)"); + + this.branch = branch; + this.branchExpressionValue = branchExpressionValue; + } + + /** + *

Getter for the field branch.

+ * + * @return a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.branch.Branch} object. + */ + public Branch getBranch() { + return branch; + } + + /** + *

Getter for the field branchExpressionValue.

+ * + * @return a boolean. + */ + public boolean getBranchExpressionValue() { + return branchExpressionValue; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + + String r = "CD " + branch; + + if (branchExpressionValue) + r += " - TRUE"; + else + r += " - FALSE"; + + return r; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ControlDependency that = (ControlDependency) o; + return branchExpressionValue == that.branchExpressionValue && + Objects.equals(branch, that.branch); + } + + @Override + public int hashCode() { + return Objects.hash(branch, branchExpressionValue); + } + +} diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/ControlFlowEdge.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/ControlFlowEdge.java new file mode 100755 index 0000000000..beaa8b3412 --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/ControlFlowEdge.java @@ -0,0 +1,119 @@ +/* + * Adapted from the EvoSuite project (https://github.com/EvoSuite/evosuite) + * and modified for use in EvoMaster's Dynamosa module. + */ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg; + +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.branch.Branch; +import org.jgrapht.graph.DefaultEdge; + +public class ControlFlowEdge extends DefaultEdge { + + private ControlDependency cd; + private boolean isExceptionEdge; + + /** + *

Constructor for ControlFlowEdge.

+ */ + public ControlFlowEdge() { + this.cd = null; + this.isExceptionEdge = false; + } + + /** + *

Constructor for ControlFlowEdge.

+ * + * @param isExceptionEdge a boolean. + */ + public ControlFlowEdge(boolean isExceptionEdge) { + this.isExceptionEdge = isExceptionEdge; + } + + /** + *

Constructor for ControlFlowEdge.

+ * + * @param cd a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.ControlDependency} object. + * @param isExceptionEdge a boolean. + */ + public ControlFlowEdge(ControlDependency cd, boolean isExceptionEdge) { + this.cd = cd; + this.isExceptionEdge = isExceptionEdge; + } + + + /** + * Sort of a copy constructor + * + * @param clone a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.ControlFlowEdge} object. + */ + public ControlFlowEdge(ControlFlowEdge clone) { + if (clone != null) { + this.cd = clone.cd; + this.isExceptionEdge = clone.isExceptionEdge; + } + } + + /** + *

getControlDependency

+ * + * @return a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.ControlDependency} object. + */ + public ControlDependency getControlDependency() { + return cd; + } + + /** + *

hasControlDependency

+ * + * @return a boolean. + */ + public boolean hasControlDependency() { + return cd != null; + } + + /** + *

getBranchInstruction

+ * + * @return a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.branch.Branch} object. + */ + public Branch getBranchInstruction() { + if (cd == null) + return null; + + return cd.getBranch(); + } + + /** + *

isExceptionEdge

+ * + * @return a boolean. + */ + public boolean isExceptionEdge() { + return isExceptionEdge; + } + + /** + *

getBranchExpressionValue

+ * + * @return a boolean. + */ + public boolean getBranchExpressionValue() { + if (hasControlDependency()) + return cd.getBranchExpressionValue(); + + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + String r = ""; + if (isExceptionEdge) + r += "E "; + if (cd != null) + r += cd.toString(); + return r; + } +} diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/ControlFlowGraph.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/ControlFlowGraph.java new file mode 100755 index 0000000000..07ac2deb19 --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/ControlFlowGraph.java @@ -0,0 +1,191 @@ +/* + * Adapted from the EvoSuite project (https://github.com/EvoSuite/evosuite) + * and modified for use in EvoMaster's Dynamosa module. + */ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg; + +import org.evomaster.client.java.instrumentation.dynamosa.graphs.EvoMasterGraph; +import org.jgrapht.graph.DefaultDirectedGraph; +import org.objectweb.asm.Opcodes; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Queue; +import java.util.Set; + + +public abstract class ControlFlowGraph extends + EvoMasterGraph { + + protected String className; + protected String methodName; + protected int access; + + /** + * Creates a fresh and empty CFG for the given class and method + * + * @param className a {@link java.lang.String} object. + * @param methodName a {@link java.lang.String} object. + * @param access a int. + */ + protected ControlFlowGraph(String className, String methodName, int access) { + super(ControlFlowEdge.class); + + if (className == null || methodName == null) + throw new IllegalArgumentException("null given"); + + this.className = className; + this.methodName = methodName; + this.access = access; + } + + /** + * Creates a CFG determined by the given jGraph for the given class and + * method + * + * @param className a {@link java.lang.String} object. + * @param methodName a {@link java.lang.String} object. + * @param access a int. + * @param jGraph a {@link org.jgrapht.graph.DefaultDirectedGraph} object. + */ + protected ControlFlowGraph(String className, String methodName, int access, + DefaultDirectedGraph jGraph) { + super(jGraph, ControlFlowEdge.class); + + if (className == null || methodName == null) + throw new IllegalArgumentException("null given"); + + this.className = className; + this.methodName = methodName; + this.access = access; + } + + /** + *

leadsToNode

+ * + * @param e a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.ControlFlowEdge} object. + * @param b a V object. + * @return a boolean. + */ + public boolean leadsToNode(ControlFlowEdge e, V b) { + + Set handled = new HashSet<>(); + + Queue queue = new LinkedList<>(); + queue.add(getEdgeTarget(e)); + while (!queue.isEmpty()) { + V current = queue.poll(); + if (handled.contains(current)) + continue; + handled.add(current); + + for (V next : getChildren(current)) + if (next.equals(b)) + return true; + else + queue.add(next); + } + + return false; + } + + // /** + // * Can be used to retrieve a Branch contained in this CFG identified by + // it's + // * branchId + // * + // * If no such branch exists in this CFG, null is returned + // */ + // public abstract BytecodeInstruction getBranch(int branchId); + + /** + * Can be used to retrieve an instruction contained in this CFG identified + * by it's instructionId + *

+ * If no such instruction exists in this CFG, null is returned + * + * @param instructionId a int. + * @return a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction} object. + */ + public abstract BytecodeInstruction getInstruction(int instructionId); + + /** + * Determines, whether a given instruction is contained in this CFG + * + * @param instruction a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction} object. + * @return a boolean. + */ + public abstract boolean containsInstruction(BytecodeInstruction instruction); + + /** + *

determineEntryPoint

+ * + * @return a V object. + */ + public V determineEntryPoint() { + Set candidates = determineEntryPoints(); + + if (candidates.size() > 1) + throw new IllegalStateException( + "expect CFG of a method to contain at most one instruction with no parent in " + + methodName); + + for (V instruction : candidates) + return instruction; + + // there was a back loop to the first instruction within this CFG, so no + // candidate + // can also happen in empty methods + return null; + } + + /** + *

Getter for the field className.

+ * + * @return a {@link java.lang.String} object. + */ + public String getClassName() { + return className; + } + + /** + *

Getter for the field methodName.

+ * + * @return a {@link java.lang.String} object. + */ + public String getMethodName() { + return methodName; + } + + /** + *

Getter for the field access.

+ * + * @return a int. + */ + public int getMethodAccess() { + return access; + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return methodName + " " + getCFGType(); + } + + /** + * {@inheritDoc} + */ + @Override + protected String dotSubFolder() { + return toFileString(className) + "/" + getCFGType() + "/"; + } + + /** + *

getCFGType

+ * + * @return a {@link java.lang.String} object. + */ + public abstract String getCFGType(); +} diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/EntryBlock.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/EntryBlock.java new file mode 100755 index 0000000000..f7ba45042a --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/EntryBlock.java @@ -0,0 +1,42 @@ +/* + * Adapted from the EvoSuite project (https://github.com/EvoSuite/evosuite) + * and modified for use in EvoMaster's Dynamosa module. + */ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg; + +public class EntryBlock extends BasicBlock { + + /** + *

Constructor for EntryBlock.

+ * + * @param className a {@link java.lang.String} object. + * @param methodName a {@link java.lang.String} object. + */ + public EntryBlock(String className, String methodName) { + super(className, methodName); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isEntryBlock() { + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return "EntryBlock for method " + methodName; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return getName(); + } +} diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/ExitBlock.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/ExitBlock.java new file mode 100755 index 0000000000..8e9f34b1d3 --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/ExitBlock.java @@ -0,0 +1,42 @@ +/* + * Adapted from the EvoSuite project (https://github.com/EvoSuite/evosuite) + * and modified for use in EvoMaster's Dynamosa module. + */ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg; + +public class ExitBlock extends BasicBlock { + + /** + *

Constructor for ExitBlock.

+ * + * @param className a {@link java.lang.String} object. + * @param methodName a {@link java.lang.String} object. + */ + public ExitBlock(String className, String methodName) { + super(className, methodName); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isExitBlock() { + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return "ExitBlock for method " + methodName; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return getName(); + } +} diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/RawControlFlowGraph.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/RawControlFlowGraph.java new file mode 100755 index 0000000000..3798ce58dc --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/RawControlFlowGraph.java @@ -0,0 +1,363 @@ +/* + * Adapted from the EvoSuite project (https://github.com/EvoSuite/evosuite) + * and modified for use in EvoMaster's Dynamosa module. + */ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg; + +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.branch.BranchPool; + +import org.evomaster.client.java.utils.SimpleLogger; + +import java.util.*; + +import static java.util.Comparator.comparingInt; + +/** + * Represents the complete CFG of a method + *

+ * Essentially this is a graph containing all BytecodeInstrucions of a method as + * nodes. From each such instruction there is an edge to each possible + * instruction the control flow can reach immediately after that instruction. + * + */ +public class RawControlFlowGraph extends ControlFlowGraph { + + private final ClassLoader classLoader; + + /** + * @return the classLoader + */ + public ClassLoader getClassLoader() { + return classLoader; + } + + /** + *

+ * Constructor for RawControlFlowGraph. + *

+ * + * @param className a {@link java.lang.String} object. + * @param methodName a {@link java.lang.String} object. + * @param access a int. + */ + public RawControlFlowGraph(ClassLoader classLoader, String className, + String methodName, int access) { + super(className, methodName, access); + this.classLoader = classLoader; + SimpleLogger.info("Creating new RawCFG for " + className + "." + methodName + ": " + this.vertexCount()); + } + + // inherited from ControlFlowGraph + + /** + * {@inheritDoc} + */ + @Override + public boolean containsInstruction(BytecodeInstruction instruction) { + + return containsVertex(instruction); + } + + /** + * {@inheritDoc} + */ + @Override + public BytecodeInstruction getInstruction(int instructionId) { + return vertexSet().stream() + .filter(v -> v.getInstructionId() == instructionId) + .findFirst() + .orElse(null); + } + + /** + *

+ * addEdge + *

+ * + * @param src a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction} object. + * @param target a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction} object. + * @param isExceptionEdge a boolean. + * @return a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.ControlFlowEdge} object. + */ + protected ControlFlowEdge addEdge(BytecodeInstruction src, + BytecodeInstruction target, boolean isExceptionEdge) { + + SimpleLogger.debug("Adding edge to RawCFG of " + className + "." + methodName + ": " + this.vertexCount()); + + if (BranchPool.getInstance(classLoader).isKnownAsBranch(src) && src.isBranch()) { + return addBranchEdge(src, target, isExceptionEdge); + } + + return addUnlabeledEdge(src, target, isExceptionEdge); + } + + private ControlFlowEdge addUnlabeledEdge(BytecodeInstruction src, + BytecodeInstruction target, boolean isExceptionEdge) { + + return internalAddEdge(src, target, new ControlFlowEdge(isExceptionEdge)); + } + + private ControlFlowEdge addBranchEdge(BytecodeInstruction src, + BytecodeInstruction target, boolean isExceptionEdge) { + + boolean isJumping = !isNonJumpingEdge(src, target); + ControlDependency cd = new ControlDependency(src.toBranch(), isJumping); + + ControlFlowEdge e = new ControlFlowEdge(cd, isExceptionEdge); + + return internalAddEdge(src, target, e); + } + + private ControlFlowEdge internalAddEdge(BytecodeInstruction src, + BytecodeInstruction target, ControlFlowEdge e) { + + if (!super.addEdge(src, target, e)) { + // TODO find out why this still happens + SimpleLogger.debug("unable to add edge from " + src.toString() + " to " + + target.toString() + " into the rawCFG of " + getMethodName()); + e = super.getEdge(src, target); + if (e == null) + throw new IllegalStateException( + "internal graph error - completely unexpected"); + } + + return e; + } + + private boolean isNonJumpingEdge(BytecodeInstruction src, // TODO move to + // ControlFlowGraph + // and implement + // analog method + // in ActualCFG + BytecodeInstruction dst) { + + return Math.abs(src.getInstructionId() - dst.getInstructionId()) == 1; + } + + // functionality used to create ActualControlFlowGraph + + /** + *

+ * determineBasicBlockFor + *

+ * + * @param instruction a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction} object. + * @return a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BasicBlock} object. + */ + public BasicBlock determineBasicBlockFor(BytecodeInstruction instruction) { + if (instruction == null) + throw new IllegalArgumentException("null given"); + + // TODO clean this up + + SimpleLogger.debug("creating basic block for " + instruction); + + List blockNodes = new ArrayList<>(); + blockNodes.add(instruction); + + Set handledChildren = new HashSet<>(); + Set handledParents = new HashSet<>(); + + Queue queue = new LinkedList<>(); + queue.add(instruction); + while (!queue.isEmpty()) { + BytecodeInstruction current = queue.poll(); + SimpleLogger.debug("handling " + current.toString()); + + // add child to queue + if (outDegreeOf(current) == 1) + for (BytecodeInstruction child : getChildren(current)) { + // this must be only one edge if inDegree was 1 + + if (blockNodes.contains(child)) + continue; + + if (handledChildren.contains(child)) + continue; + handledChildren.add(child); + + if (inDegreeOf(child) < 2) { + // insert child right after current + // ... always thought ArrayList had insertBefore() and + // insertAfter() methods ... well + blockNodes.add(blockNodes.indexOf(current) + 1, child); + + SimpleLogger.debug(" added child to queue: " + child.toString()); + queue.add(child); + } + } + + // add parent to queue + if (inDegreeOf(current) == 1) + for (BytecodeInstruction parent : getParents(current)) { + // this must be only one edge if outDegree was 1 + + if (blockNodes.contains(parent)) + continue; + + if (handledParents.contains(parent)) + continue; + handledParents.add(parent); + + if (outDegreeOf(parent) < 2) { + // insert parent right before current + blockNodes.add(blockNodes.indexOf(current), parent); + + SimpleLogger.debug(" added parent to queue: " + parent.toString()); + queue.add(parent); + } + } + } + + BasicBlock r = new BasicBlock(classLoader, className, methodName, blockNodes); + + SimpleLogger.debug("created nodeBlock: " + r); + return r; + } + + /** + * {@inheritDoc} + */ + @Override + public BytecodeInstruction determineEntryPoint() { + + BytecodeInstruction noParent = super.determineEntryPoint(); + if (noParent != null) + return noParent; + + // copied from ControlFlowGraph.determineEntryPoint(): + // there was a back loop to the first instruction within this CFG, so no + // candidate + + return getInstructionWithSmallestId(); + } + + /** + * {@inheritDoc} + */ + @Override + public Set determineExitPoints() { + + Set r = super.determineExitPoints(); + + // if the last instruction loops back to a previous instruction there is + // no node without a child, so just take the last byteCode instruction + + if (r.isEmpty()) + r.add(getInstructionWithBiggestId()); + + return r; + + } + + /** + *

+ * determineBranches + *

+ * + * @return Set containing all nodes with out degree > 1 + */ + public Set determineBranches() { + Set r = new HashSet<>(); + for (BytecodeInstruction instruction : vertexSet()) + if (outDegreeOf(instruction) > 1) + r.add(instruction); + return r; + } + + /** + *

+ * determineJoins + *

+ * + * @return Set containing all nodes with in degree > 1 + */ + public Set determineJoins() { + Set r = new HashSet<>(); + for (BytecodeInstruction instruction : vertexSet()) + if (inDegreeOf(instruction) > 1) + r.add(instruction); + return r; + } + + /** + *

+ * getInstructionWithSmallestId + *

+ * + * @return a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction} object. + */ + public BytecodeInstruction getInstructionWithSmallestId() { + return vertexSet().stream() + .min(comparingInt(BytecodeInstruction::getInstructionId)) + .orElse(null); + } + + /** + *

+ * getInstructionWithBiggestId + *

+ * + * @return a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction} object. + */ + public BytecodeInstruction getInstructionWithBiggestId() { + return vertexSet().stream() + .max(comparingInt(BytecodeInstruction::getInstructionId)) + .orElse(null); + } + + /** + * In some cases there can be isolated nodes within a CFG. For example in an + * completely empty try-catch-finally. Since these nodes are not reachable + * but cause trouble when determining the entry point of a CFG they get + * removed. + * + * @return a int. + */ + public int removeIsolatedNodes() { + Set candidates = determineEntryPoints(); + + int removed = 0; + if (candidates.size() > 1) { + + for (BytecodeInstruction instruction : candidates) { + if (outDegreeOf(instruction) == 0 && graph.removeVertex(instruction)) { + removed++; + BytecodeInstructionPool.getInstance(classLoader).forgetInstruction(instruction); + } + } + + } + return removed; + } + + // miscellaneous + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + StringBuffer sb = new StringBuffer(); + for (ControlFlowEdge e : graph.edgeSet()) { + sb.append(graph.getEdgeSource(e) + " -> " + graph.getEdgeTarget(e)); + sb.append("\n"); + } + return sb.toString(); + } + + /** + * {@inheritDoc} + */ + @Override + public String getCFGType() { + return "RCFG"; + } + + /** + * {@inheritDoc} + */ + public boolean addVertex(BytecodeInstruction ins) { + return super.addVertex(ins); + } +} diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/branch/Branch.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/branch/Branch.java new file mode 100755 index 0000000000..9ec1297d34 --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/branch/Branch.java @@ -0,0 +1,173 @@ +/* + * Adapted from the EvoSuite project (https://github.com/EvoSuite/evosuite) + * and modified for use in EvoMaster's Dynamosa module. + */ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.branch; + +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction; + +import java.io.Serializable; + +/** + * An object of this class corresponds to a Branch inside the class under test. + * + *

+ * Branches are created by the {@code CFGMethodVisitor} via the {@code BranchPool}. Each Branch + * holds its corresponding {@code BytecodeInstruction} from the {@code RawControlFlowGraph} and + * is associated with a unique {@code actualBranchId}. + * + *

+ * A Branch can either come from a jump instruction, as defined in + * {@code BytecodeInstruction.isBranch()}. + * Only {@code BytecodeInstructions} satisfying {@code BytecodeInstruction.isActualbranch()} are + * expected to be associated with a {@code Branch} object. + * + */ +public class Branch implements Serializable { + + private static final long serialVersionUID = -4732587925060748263L; + + private final int actualBranchId; + + private final BytecodeInstruction instruction; + + /** + * Canonical identifiers matching the descriptive ids used in {@code ObjectiveNaming}. + */ + private String thenObjectiveId; + private String elseObjectiveId; + + /** + * Constructor for usual jump instruction Branches. + * + * @param branchInstruction a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction} object. + * @param actualBranchId a int. + */ + public Branch(BytecodeInstruction branchInstruction, int actualBranchId) { + if (!branchInstruction.isBranch()) + throw new IllegalArgumentException("only branch instructions are accepted"); + + this.instruction = branchInstruction; + this.actualBranchId = actualBranchId; + + if (this.actualBranchId < 1) + throw new IllegalStateException( + "expect branch to have actualBranchId set to positive value"); + } + + /** + *

+ * Getter for the field actualBranchId. + *

+ * + * @return a int. + */ + public int getActualBranchId() { + return actualBranchId; + } + + /** + *

+ * Getter for the field instruction. + *

+ * + * @return a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction} object. + */ + public BytecodeInstruction getInstruction() { + return instruction; + } + + /** + *

+ * getClassName + *

+ * + * @return a {@link java.lang.String} object. + */ + public String getClassName() { + return instruction.getClassName(); + } + + /** + *

+ * getMethodName + *

+ * + * @return a {@link java.lang.String} object. + */ + public String getMethodName() { + return instruction.getMethodName(); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + actualBranchId; + result = prime * result + ((instruction == null) ? 0 : instruction.hashCode()); + return result; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Branch other = (Branch) obj; + if (actualBranchId != other.actualBranchId) + return false; + if (instruction == null) { + if (other.instruction != null) + return false; + } else if (!instruction.equals(other.instruction)) + return false; + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + String r = "I" + instruction.getInstructionId(); + r += " Branch " + getActualBranchId(); + r += " " + instruction.getInstructionType(); + r += " L" + instruction.getLineNumber(); + + if (thenObjectiveId != null || elseObjectiveId != null) { + r += " [" + (thenObjectiveId == null ? "null" : thenObjectiveId) + + ", " + (elseObjectiveId == null ? "null" : elseObjectiveId) + "]"; + } + + return r; + } + + /** + * Store the descriptive identifiers associated with this branch. + * + * @param thenId descriptive id for the "true" outcome + * @param elseId descriptive id for the "false" outcome + */ + public void setObjectiveIds(String thenId, String elseId) { + this.thenObjectiveId = thenId; + this.elseObjectiveId = elseId; + } + + public String getThenObjectiveId() { + return thenObjectiveId; + } + + public String getElseObjectiveId() { + return elseObjectiveId; + } + +} diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/branch/BranchPool.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/branch/BranchPool.java new file mode 100755 index 0000000000..dbd5c2190f --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/branch/BranchPool.java @@ -0,0 +1,179 @@ +/* + * Adapted from the EvoSuite project (https://github.com/EvoSuite/evosuite) + * and modified for use in EvoMaster's Dynamosa module. + */ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.branch; + +import com.google.common.annotations.VisibleForTesting; +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction; +import org.evomaster.client.java.instrumentation.shared.ObjectiveNaming; +import org.evomaster.client.java.utils.SimpleLogger; + +import java.util.HashMap; +import java.util.Map; + +/** + * This class is supposed to hold all the available information concerning + * Branches. + *

+ * The addBranch()-Method gets called during class analysis. Whenever the + * BytecodeInstructionPool detects a BytecodeInstruction that corresponds to a + * Branch in the class under test as defined in + * BytecodeInstruction.isActualBranch() it calls the registerAsBranch() method + * of this class which in turn properly registers the instruction within this + * pool. + *

+ * + */ +public class BranchPool { + + // maps all known branch instructions to their branchId + private final Map registeredNormalBranches = new HashMap<>(); + + // number of known Branches - used for actualBranchIds + private int branchCounter = 0; + + private static final Map instanceMap = new HashMap<>(); + + private final Map branchOrdinalCounters = new HashMap<>(); + + public static BranchPool getInstance(ClassLoader classLoader) { + if (!instanceMap.containsKey(classLoader)) { + instanceMap.put(classLoader, new BranchPool()); + } + + return instanceMap.get(classLoader); + } + + @VisibleForTesting + public static void resetForTesting(ClassLoader classLoader) { + BranchPool pool = instanceMap.remove(classLoader); + if (pool != null) { + pool.registeredNormalBranches.clear(); + pool.branchOrdinalCounters.clear(); + pool.branchCounter = 0; + } + } + // fill the pool + + + + /** + * Called by the BytecodeInstructionPool whenever it detects an instruction + * that corresponds to a Branch in the class under test as defined by + * BytecodeInstruction.isActualBranch(). + * + * @param instruction a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction} object. + */ + public void registerAsBranch(BytecodeInstruction instruction) { + if (!(instruction.isBranch())) + throw new IllegalArgumentException("CFGVertex of a branch expected"); + if (isKnownAsBranch(instruction)) + return; + + registerInstruction(instruction); + + } + + private void registerInstruction(BytecodeInstruction v) { + if (isKnownAsBranch(v)) + throw new IllegalStateException( + "expect registerInstruction() to be called at most once for each instruction"); + + if (!v.isBranch()) + throw new IllegalArgumentException("expect given instruction to be a normal branch"); + + registerNormalBranchInstruction(v); + } + + private void registerNormalBranchInstruction(BytecodeInstruction v) { + if (!v.isBranch()) + throw new IllegalArgumentException("normal branch instruction expceted"); + + if (registeredNormalBranches.containsKey(v)) + throw new IllegalArgumentException( + "instruction already registered as a normal branch"); + + branchCounter++; + Branch b = new Branch(v, branchCounter); + attachObjectiveIds(b); + registeredNormalBranches.put(v, b); + SimpleLogger.info("Branch " + branchCounter + " at line " + v.getLineNumber()); + } + + private void attachObjectiveIds(Branch branch) { + if (branch == null) { + return; + } + BytecodeInstruction instruction = branch.getInstruction(); + if (instruction == null || !instruction.isBranch()) { + return; + } + try { + if (!instruction.hasLineNumberSet()) { + SimpleLogger.warn("Cannot attach objective ids to branch without line number: " + branch); + return; + } + int lineNumber = instruction.getLineNumber(); + if (lineNumber <= 0) { + SimpleLogger.warn("Invalid line number while attaching objective ids to branch: " + branch); + return; + } + String className = instruction.getClassName(); + String methodName = instruction.getMethodName(); + int ordinal = nextOrdinalForLine(className, methodName, lineNumber); + int opcode = instruction.getASMNode().getOpcode(); + String thenId = ObjectiveNaming.branchObjectiveName(className, lineNumber, ordinal, true, opcode); + String elseId = ObjectiveNaming.branchObjectiveName(className, lineNumber, ordinal, false, opcode); + branch.setObjectiveIds(thenId, elseId); + } catch (Exception e) { + SimpleLogger.warn("Failed to attach objective ids to branch " + branch + ": " + e.getMessage()); + } + } + + private int nextOrdinalForLine(String className, String methodName, int lineNumber) { + String key = className + "#" + methodName + "#" + lineNumber; + Integer ordinal = branchOrdinalCounters.getOrDefault(key, 0); + branchOrdinalCounters.put(key, ordinal + 1); + return ordinal; + } + + // retrieve information from the pool + + /** + * Checks whether the given instruction has Branch objects associated with + * it. + *

+ * Returns true if the given BytecodeInstruction previously passed a call to + * registerAsBranch(instruction), false otherwise + * + * @param instruction a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction} object. + * @return a boolean. + */ + public boolean isKnownAsBranch(BytecodeInstruction instruction) { + return registeredNormalBranches.containsKey(instruction); + } + + /** + *

+ * getBranchForInstruction + *

+ * + * @param instruction a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction} object. + * @return a {@link org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.branch.Branch} object. + */ + public Branch getBranchForInstruction(BytecodeInstruction instruction) { + if (instruction == null) + throw new IllegalArgumentException("null given"); + if (!isKnownAsBranch(instruction)) + throw new IllegalArgumentException( + "expect given instruction to be known as a normal branch"); + + Branch branch = registeredNormalBranches.get(instruction); + if (branch == null) { + throw new IllegalStateException("Branch not found for instruction"); + } + return branch; + } + +} diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/visitor/CFGClassVisitor.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/visitor/CFGClassVisitor.java new file mode 100644 index 0000000000..676da385bf --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/visitor/CFGClassVisitor.java @@ -0,0 +1,47 @@ +/* + * Adapted from the EvoSuite project (https://github.com/EvoSuite/evosuite) + * and modified for use in EvoMaster's Dynamosa module. + */ +package org.evomaster.client.java.instrumentation.dynamosa.visitor; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +/** + * Minimal class visitor that wraps each method with CFGMethodVisitor to build graphs, + * while delegating to the downstream visitor chain unchanged. + */ +public class CFGClassVisitor extends ClassVisitor { + + private final ClassLoader classLoader; + private String classNameWithDots; + + public CFGClassVisitor(ClassLoader classLoader, ClassVisitor cv) { + super(Opcodes.ASM9, cv); + this.classLoader = classLoader; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + this.classNameWithDots = name == null ? null : name.replace('/', '.'); + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodVisitor downstream = super.visitMethod(access, name, descriptor, signature, exceptions); + return new CFGMethodVisitor( + classLoader, + classNameWithDots, + access, + name, + descriptor, + signature, + exceptions, + downstream + ); + } +} + + diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/visitor/CFGMethodVisitor.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/visitor/CFGMethodVisitor.java new file mode 100755 index 0000000000..da6aed4f3c --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/dynamosa/visitor/CFGMethodVisitor.java @@ -0,0 +1,132 @@ +/* + * Adapted from the EvoSuite project (https://github.com/EvoSuite/evosuite) + * and modified for use in EvoMaster's Dynamosa module. + */ +package org.evomaster.client.java.instrumentation.dynamosa.visitor; + +import org.evomaster.client.java.instrumentation.dynamosa.AnnotatedMethodNode; +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.CFGGenerator; +import org.objectweb.asm.*; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.analysis.Analyzer; +import org.objectweb.asm.tree.analysis.AnalyzerException; +import org.objectweb.asm.tree.analysis.SourceInterpreter; +import org.objectweb.asm.tree.analysis.SourceValue; +import org.evomaster.client.java.utils.SimpleLogger; + +import java.util.Arrays; +import java.util.List; + +/** + * Create a minimized control flow graph for the method and store it. In + * addition, this visitor also adds instrumentation for branch distance + * measurement + *

+ * defUse, concurrency and LCSAJs instrumentation is also added (if the + * properties are set). + * + */ +public class CFGMethodVisitor extends MethodVisitor { + + /** + * Methods to skip during CFG analysis. + */ + public static final List EXCLUDE = Arrays.asList("()V"); + + /** + * This is the name + the description of the method. It is more like the + * signature and less like the name. The name of the method can be found in + * this.plain_name + */ + private final String methodName; + + private final MethodVisitor next; + private final int access; + private final String className; + private final ClassLoader classLoader; + + // no line tracking needed here + + /** + *

+ * Constructor for CFGMethodVisitor. + *

+ * + * @param className a {@link java.lang.String} object. + * @param access a int. + * @param name a {@link java.lang.String} object. + * @param desc a {@link java.lang.String} object. + * @param signature a {@link java.lang.String} object. + * @param exceptions an array of {@link java.lang.String} objects. + * @param mv a {@link org.objectweb.asm.MethodVisitor} object. + */ + public CFGMethodVisitor(ClassLoader classLoader, String className, int access, + String name, String desc, String signature, String[] exceptions, + MethodVisitor mv) { + + // super(new MethodNode(access, name, desc, signature, exceptions), + // className, + // name.replace('/', '.'), null, desc); + + super(Opcodes.ASM9, new AnnotatedMethodNode(access, name, desc, signature, + exceptions)); + + this.next = mv; + this.className = className; // .replace('/', '.'); + this.access = access; + this.methodName = name + desc; + this.classLoader = classLoader; + } + + /** + * {@inheritDoc} + */ + @Override + public void visitEnd() { + SimpleLogger.debug("Creating CFG of " + className + "." + methodName); + MethodNode mn = (AnnotatedMethodNode) mv; + // skip excluded, abstract or native methods + if (EXCLUDE.contains(methodName) + || (access & Opcodes.ACC_ABSTRACT) != 0 + || (access & Opcodes.ACC_NATIVE) != 0) { + mn.accept(next); + return; + } + SimpleLogger.info("Analyzing method " + methodName + " in class " + className); + CFGGenerator cfgGenerator = new CFGGenerator(classLoader, className, methodName, mn); + SimpleLogger.info("Generating CFG for method " + methodName); + try { + Analyzer analyzer = new Analyzer(new SourceInterpreter()) { + @Override + protected void newControlFlowEdge(int src, int dst) { + cfgGenerator.registerControlFlowEdge(src, dst, getFrames(), false); + } + + @Override + protected boolean newControlFlowExceptionEdge(int src, int dst) { + cfgGenerator.registerControlFlowEdge(src, dst, getFrames(), true); + return true; + } + }; + analyzer.analyze(className, mn); + SimpleLogger.debug("Method graph for " + + className + + "." + + methodName + + " contains " + + cfgGenerator.getRawGraph().vertexSet().size() + + " nodes for " + analyzer.getFrames().length + + " instructions"); + // compute Raw and ActualCFG and put both into GraphPool + cfgGenerator.registerCFGs(); + SimpleLogger.info("Created CFG for method " + methodName); + } catch (AnalyzerException e) { + SimpleLogger.error("Analyzer exception while analyzing " + className + "." + + methodName + ": " + e); + e.printStackTrace(); + } + mn.accept(next); + } + + // removed auxiliary methods related to method listings and filters +} diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/AgentController.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/AgentController.java index 0ecdbe7f6d..e1a3e3a6e7 100644 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/AgentController.java +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/AgentController.java @@ -4,12 +4,15 @@ import org.evomaster.client.java.instrumentation.InstrumentationController; import org.evomaster.client.java.instrumentation.staticstate.UnitsInfoRecorder; import org.evomaster.client.java.utils.SimpleLogger; +import org.evomaster.client.java.instrumentation.dynamosa.DynamosaConfig; +import org.evomaster.client.java.instrumentation.external.DynamosaControlDependenceSnapshot; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.Socket; import java.util.Collection; +import java.util.Collections; import java.util.List; /** @@ -65,6 +68,10 @@ public static void start(int port){ InstrumentationController.resetForNewSearch(); sendCommand(Command.ACK); break; + case SET_DYNAMOSA_CONFIG: + handleDynamosaConfig(); + sendCommand(Command.ACK); + break; case NEW_TEST: InstrumentationController.resetForNewTest(); sendCommand(Command.ACK); @@ -108,6 +115,9 @@ public static void start(int port){ case EXTRACT_JVM_DTO: handleExtractingSpecifiedDto(); break; + case DYNAMOSA_CDG_SNAPSHOT: + handleDynamosaControlDependenceSnapshot(); + break; default: SimpleLogger.error("Unrecognized command: "+command); return; @@ -121,6 +131,21 @@ public static void start(int port){ thread.start(); } + private static void handleDynamosaConfig() { + try { + Object msg = in.readObject(); + DynamosaConfigDto dto = (DynamosaConfigDto) msg; + if (dto.enableGraphs != null) { + DynamosaConfig.setEnableGraphs(dto.enableGraphs); + } + if (dto.writeCfg != null) { + DynamosaConfig.setWriteCfgEnabled(dto.writeCfg); + } + } catch (Exception e) { + SimpleLogger.error("Failure in handling DYNAMOSA config: "+e.getMessage()); + } + } + private static void sendCommand(Command command){ try { @@ -226,6 +251,24 @@ private static void handleExtractingSpecifiedDto(){ } } + private static void handleDynamosaControlDependenceSnapshot(){ + try { + Object msg = in.readObject(); + int fromIndex = 0; + if (msg instanceof Integer) { + fromIndex = (Integer) msg; + } + DynamosaControlDependenceSnapshot snapshot = InstrumentationController.getControlDependenceSnapshot(fromIndex); + sendObject(snapshot); + } catch (Exception e){ + SimpleLogger.error("Failure in handling Dynamosa CDG snapshot: "+e.getMessage()); + try { + sendObject(new DynamosaControlDependenceSnapshot(Collections.emptyList(), 0)); + } catch (IOException ignored) { + } + } + } + private static void handleTargetInfos() { diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/Command.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/Command.java index afb9ba7484..8448c97096 100644 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/Command.java +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/Command.java @@ -22,5 +22,7 @@ public enum Command implements Serializable { EXECUTING_ACTION, BOOT_TIME_INFO, EXTRACT_JVM_DTO, - BOOTING_SUT + BOOTING_SUT, + SET_DYNAMOSA_CONFIG, + DYNAMOSA_CDG_SNAPSHOT } diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/DynamosaConfigDto.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/DynamosaConfigDto.java new file mode 100644 index 0000000000..fe5c54fdb3 --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/DynamosaConfigDto.java @@ -0,0 +1,8 @@ +package org.evomaster.client.java.instrumentation.external; + +import java.io.Serializable; + +public class DynamosaConfigDto implements Serializable { + public Boolean enableGraphs; + public Boolean writeCfg; +} \ No newline at end of file diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/DynamosaControlDependenceSnapshot.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/DynamosaControlDependenceSnapshot.java new file mode 100644 index 0000000000..42b6f63572 --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/DynamosaControlDependenceSnapshot.java @@ -0,0 +1,36 @@ +package org.evomaster.client.java.instrumentation.external; + +import org.evomaster.client.java.instrumentation.shared.dto.ControlDependenceGraphDto; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Serializable payload used between the Java Agent and the controller to + * exchange newly discovered control-dependence graphs together with the + * next index to request. + */ +public class DynamosaControlDependenceSnapshot implements Serializable { + + private static final long serialVersionUID = 6779255129756570792L; + + private final List graphs; + private final int nextIndex; + + public DynamosaControlDependenceSnapshot(List graphs, int nextIndex) { + this.graphs = graphs == null ? new ArrayList<>() : new ArrayList<>(graphs); + this.nextIndex = Math.max(nextIndex, 0); + } + + public List getGraphs() { + return Collections.unmodifiableList(graphs); + } + + public int getNextIndex() { + return nextIndex; + } +} + + diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/ServerController.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/ServerController.java index 5df63053ce..50765e0be0 100644 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/ServerController.java +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/ServerController.java @@ -1,6 +1,7 @@ package org.evomaster.client.java.instrumentation.external; import org.evomaster.client.java.instrumentation.*; +import org.evomaster.client.java.instrumentation.shared.dto.ControlDependenceGraphDto; import org.evomaster.client.java.instrumentation.staticstate.UnitsInfoRecorder; import org.evomaster.client.java.utils.SimpleLogger; @@ -11,6 +12,7 @@ import java.net.ServerSocket; import java.net.Socket; import java.util.Collection; +import java.util.Collections; import java.util.List; /** @@ -34,6 +36,7 @@ public class ServerController { private Socket socket; protected ObjectOutputStream out; protected ObjectInputStream in; + private int dynamosaCdgIndex = 0; public synchronized int startServer() { @@ -46,6 +49,7 @@ public synchronized int startServer() { throw new IllegalStateException(e); } + dynamosaCdgIndex = 0; return server.getLocalPort(); } @@ -58,6 +62,7 @@ public synchronized void closeServer() { socket = null; in = null; out = null; + dynamosaCdgIndex = 0; } catch (IOException e) { throw new IllegalStateException(e); } @@ -216,6 +221,10 @@ public boolean setBootingSut(boolean isBooting){ return sendWithDataAndExpectACK(Command.BOOTING_SUT, isBooting); } + public boolean setDynamosaConfig(DynamosaConfigDto dto){ + return sendWithDataAndExpectACK(Command.SET_DYNAMOSA_CONFIG, dto); + } + // public synchronized List getAllCoveredTargetsInfo() { // boolean sent = sendCommand(Command.ALL_COVERED_TARGETS_INFO); // if (!sent) { @@ -291,6 +300,34 @@ public synchronized List getAdditionalInfoList() { return (List) response; } + public synchronized List getDynamosaControlDependenceGraphs() { + + boolean sent = sendCommand(Command.DYNAMOSA_CDG_SNAPSHOT); + if (!sent) { + SimpleLogger.error("Failed to send Dynamosa CDG request"); + return Collections.emptyList(); + } + + if (!sendObject(dynamosaCdgIndex)) { + SimpleLogger.error("Failed to send Dynamosa CDG index"); + return Collections.emptyList(); + } + + Object response = waitAndGetResponse(); + if (response == null) { + SimpleLogger.error("Failed to read Dynamosa CDG response"); + return Collections.emptyList(); + } + + if (!(response instanceof DynamosaControlDependenceSnapshot)) { + throw new IllegalStateException(errorMsgExpectingResponse(response, DynamosaControlDependenceSnapshot.class.getSimpleName())); + } + + DynamosaControlDependenceSnapshot snapshot = (DynamosaControlDependenceSnapshot) response; + dynamosaCdgIndex = snapshot.getNextIndex(); + return snapshot.getGraphs(); + } + public synchronized BootTimeObjectiveInfo handleBootTimeObjectiveInfo() { boolean sent = sendCommand(Command.BOOT_TIME_INFO); diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/GraphPoolTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/GraphPoolTest.java new file mode 100644 index 0000000000..5bf7a62949 --- /dev/null +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/GraphPoolTest.java @@ -0,0 +1,155 @@ +package org.evomaster.client.java.instrumentation.dynamosa.graphs; + +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.ActualControlFlowGraph; +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction; +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.RawControlFlowGraph; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.InsnNode; + +import static org.junit.jupiter.api.Assertions.*; + +class GraphPoolTest { + + private static final ClassLoader TEST_LOADER = GraphPoolTest.class.getClassLoader(); + private static final String CLASS_NAME = "com.example.GraphPoolFixture"; + private static final String METHOD_NAME = "sample()V"; + + // Custom class loader that intentionally throws an exception when the ExecutionTracer class is loaded. + private ClassLoader blockingLoader; + + @AfterEach + void resetPools() { + GraphPool.resetForTesting(TEST_LOADER); + if (blockingLoader != null) { + GraphPool.resetForTesting(blockingLoader); + blockingLoader = null; + } + } + + @Test + void getInstanceReturnsSingletonPerLoader() { + GraphPool first = GraphPool.getInstance(TEST_LOADER); + GraphPool second = GraphPool.getInstance(TEST_LOADER); + assertSame(first, second); + + ClassLoader otherLoader = new SimpleDelegatingClassLoader(TEST_LOADER); + GraphPool third = GraphPool.getInstance(otherLoader); + assertNotSame(first, third); + GraphPool.resetForTesting(otherLoader); + } + + @Test + void getRawCFGReturnsNullWhenClassIsUnknown() { + GraphPool pool = GraphPool.getInstance(TEST_LOADER); + assertNull(pool.getRawCFG("unknown.class", METHOD_NAME)); + } + + @Test + void registerRawCFGStoresGraphUntilCleared() { + GraphPool pool = GraphPool.getInstance(TEST_LOADER); + RawControlFlowGraph raw = simpleRawGraph(TEST_LOADER, CLASS_NAME, METHOD_NAME); + + // Test that registerRawCFG and GetRawCFG works correctly + pool.registerRawCFG(raw); + assertSame(raw, pool.getRawCFG(CLASS_NAME, METHOD_NAME)); + + // Test that clear works correctly + pool.clear(CLASS_NAME, METHOD_NAME); + assertNull(pool.getRawCFG(CLASS_NAME, METHOD_NAME)); + } + + @Test + void registerActualCFGSkipsCdgWhenInstrumentationDisabled() { + blockingLoader = new NoExecutionTracerClassLoader(GraphPoolTest.class.getClassLoader()); + GraphPool pool = GraphPool.getInstance(blockingLoader); + RawControlFlowGraph raw = simpleRawGraph(blockingLoader, CLASS_NAME, METHOD_NAME); + ActualControlFlowGraph cfg = new ActualControlFlowGraph(raw); + + pool.registerActualCFG(cfg); + + assertSame(cfg, pool.getActualCFG(CLASS_NAME, METHOD_NAME)); + assertNull(pool.getCDG(CLASS_NAME, METHOD_NAME)); + } + + @Test + void resetForTestingDropsCachedInstancesAndGraphs() { + GraphPool pool = GraphPool.getInstance(TEST_LOADER); + RawControlFlowGraph raw = simpleRawGraph(TEST_LOADER, CLASS_NAME, METHOD_NAME); + pool.registerRawCFG(raw); + + GraphPool.resetForTesting(TEST_LOADER); + + GraphPool fresh = GraphPool.getInstance(TEST_LOADER); + assertNotSame(pool, fresh); + assertNull(fresh.getRawCFG(CLASS_NAME, METHOD_NAME)); + } + + private static RawControlFlowGraph simpleRawGraph(ClassLoader loader, + String className, + String methodName) { + TestRawControlFlowGraph raw = new TestRawControlFlowGraph(loader, className, methodName); + + BytecodeInstruction entry = instruction(loader, className, methodName, 0, Opcodes.NOP); + BytecodeInstruction body = instruction(loader, className, methodName, 1, Opcodes.NOP); + BytecodeInstruction exit = instruction(loader, className, methodName, 2, Opcodes.RETURN); + + raw.addVertex(entry); + raw.addVertex(body); + raw.addVertex(exit); + + raw.connect(entry, body); + raw.connect(body, exit); + + return raw; + } + + private static BytecodeInstruction instruction(ClassLoader loader, + String className, + String methodName, + int instructionId, + int opcode) { + BytecodeInstruction instruction = new BytecodeInstruction( + loader, + className, + methodName, + instructionId, + instructionId, + new InsnNode(opcode) + ); + instruction.setLineNumber(100 + instructionId); + return instruction; + } + + private static final class TestRawControlFlowGraph extends RawControlFlowGraph { + private TestRawControlFlowGraph(ClassLoader loader, String className, String methodName) { + super(loader, className, methodName, Opcodes.ACC_PUBLIC); + } + + private void connect(BytecodeInstruction src, BytecodeInstruction dst) { + super.addEdge(src, dst, false); + } + } + + private static class SimpleDelegatingClassLoader extends ClassLoader { + private SimpleDelegatingClassLoader(ClassLoader parent) { + super(parent); + } + } + + private static class NoExecutionTracerClassLoader extends ClassLoader { + private NoExecutionTracerClassLoader(ClassLoader parent) { + super(parent); + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if ("org.evomaster.client.java.instrumentation.staticstate.ExecutionTracer".equals(name)) { + throw new ClassNotFoundException(name); + } + return super.loadClass(name, resolve); + } + } +} + diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cdg/ControlDependenceGraphTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cdg/ControlDependenceGraphTest.java new file mode 100644 index 0000000000..ca564bdc67 --- /dev/null +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cdg/ControlDependenceGraphTest.java @@ -0,0 +1,229 @@ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cdg; + +import org.evomaster.client.java.instrumentation.dynamosa.graphs.GraphPool; +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.ActualControlFlowGraph; +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BasicBlock; +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction; +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.ControlDependency; +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.RawControlFlowGraph; +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.branch.Branch; +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.branch.BranchPool; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.JumpInsnNode; +import org.objectweb.asm.tree.LabelNode; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for {@link ControlDependenceGraph}. + */ +class ControlDependenceGraphTest { + + private static final ClassLoader TEST_LOADER = ControlDependenceGraphTest.class.getClassLoader(); + private static final String CLASS_NAME = "com.example.DynamosaTestClass"; + private static final String METHOD_NAME = "sample()V"; + + @AfterEach + void resetPools() { + resetBranchPool(); + resetGraphPool(); + } + + @Test + void testConstructor() { + GraphFixture fixture = GraphFixture.build(); + ControlDependenceGraph cdg = new ControlDependenceGraph(fixture.cfg); + assertEquals(fixture.cfg.getClassName(), cdg.getClassName()); + assertEquals(fixture.cfg.getMethodName(), cdg.getMethodName()); + assertEquals(fixture.cfg, cdg.getCFG()); + + // Synthetic entry block is the root node in the new graph. + BasicBlock syntheticEntry = cdg.vertexSet().stream() + .filter(BasicBlock::isEntryBlock) + .findFirst() + .orElseThrow(() -> new AssertionError("Synthetic entry block not found")); + + // Vertexes should match the CFG minus the exit block + Set expectedVertices = new java.util.HashSet<>(fixture.cfg.vertexSet()); + expectedVertices.remove(fixture.exitBlock); + assertEquals(expectedVertices, cdg.vertexSet()); + + // branch -> true path + // branch -> false path + assertTrue(cdg.containsEdge(fixture.branchBlock, fixture.trueBlock)); + assertTrue(cdg.containsEdge(fixture.branchBlock, fixture.falseBlock)); + + // synthetic entry -> entry block + // synthetic entry -> branch block + assertTrue(cdg.containsEdge(syntheticEntry, fixture.entryBlock)); + assertTrue(cdg.containsEdge(syntheticEntry, fixture.branchBlock)); + + } + + @Test + void testGetControlDependentBranchesThrowsIllegalArgumentExceptionForNullBlock() { + GraphFixture fixture = GraphFixture.build(); + ControlDependenceGraph cdg = new ControlDependenceGraph(fixture.cfg); + assertThrows(IllegalArgumentException.class, () -> cdg.getControlDependentBranches(null)); + } + + @Test + void testGetControlDependentBranchesThrowsUnknownBlockExceptionForABlockThaIsNotPartOfTheCFG() { + GraphFixture fixture = GraphFixture.build(); + ControlDependenceGraph cdg = new ControlDependenceGraph(fixture.cfg); + // Here we are creating a mock block that is not part of the fixture.cfg, so + BasicBlock foreign = org.mockito.Mockito.mock(BasicBlock.class); + assertThrows(IllegalArgumentException.class, () -> cdg.getControlDependentBranches(foreign)); + } + + @Test + void testGetControlDependenciesReturnsTheCorrectDependenciesForEveryBlock() { + GraphFixture fixture = GraphFixture.build(); + ControlDependenceGraph cdg = new ControlDependenceGraph(fixture.cfg); + + // Entry block should have no dependencies + Set entryDeps = cdg.getControlDependentBranches(fixture.entryBlock); + assertTrue(entryDeps.isEmpty()); + + // Branch block should have no dependencies + Set branchDeps = cdg.getControlDependentBranches(fixture.branchBlock); + assertTrue(branchDeps.isEmpty()); + + // True block should have a dependency on the branch + Set trueDeps = cdg.getControlDependentBranches(fixture.trueBlock); + assertEquals(1, trueDeps.size()); + assertTrue(trueDeps.contains(new ControlDependency(fixture.branch, true))); + + // False block should have a dependency on the branch + Set falseDeps = cdg.getControlDependentBranches(fixture.falseBlock); + assertEquals(1, falseDeps.size()); + assertTrue(falseDeps.contains(new ControlDependency(fixture.branch, false))); + } + + private static void resetBranchPool() { + BranchPool.resetForTesting(TEST_LOADER); + } + + private static void resetGraphPool() { + GraphPool.resetForTesting(TEST_LOADER); + } + + private static final class GraphFixture { + final ActualControlFlowGraph cfg; + final BasicBlock branchBlock; + final BasicBlock trueBlock; + final BasicBlock falseBlock; + final BasicBlock exitBlock; + final Branch branch; + final BasicBlock entryBlock; + + private GraphFixture(ActualControlFlowGraph cfg, + BasicBlock branchBlock, + BasicBlock trueBlock, + BasicBlock falseBlock, + BasicBlock exitBlock, + Branch branch, + BasicBlock entryBlock) { + this.cfg = cfg; + this.branchBlock = branchBlock; + this.trueBlock = trueBlock; + this.falseBlock = falseBlock; + this.exitBlock = exitBlock; + this.branch = branch; + this.entryBlock = entryBlock; + } + + static GraphFixture build() { + + // entry -> branch + // branch -> true path + // branch -> false path + // true path -> exit + // false path -> exit + + TestRawControlFlowGraph raw = new TestRawControlFlowGraph(); + + BytecodeInstruction entry = instruction(0, Opcodes.NOP); + BytecodeInstruction branchInsn = branchInstruction(1); + BytecodeInstruction falsePath = instruction(2, Opcodes.NOP); + BytecodeInstruction truePath = instruction(10, Opcodes.NOP); + BytecodeInstruction exit = instruction(20, Opcodes.RETURN); + + raw.addVertex(entry); + raw.addVertex(branchInsn); + raw.addVertex(falsePath); + raw.addVertex(truePath); + raw.addVertex(exit); + + raw.connect(entry, branchInsn); + raw.connect(branchInsn, truePath); // jump (true) + raw.connect(branchInsn, falsePath); // fall-through (false) + raw.connect(truePath, exit); + raw.connect(falsePath, exit); + + ActualControlFlowGraph cfg = new ActualControlFlowGraph(raw); + + BasicBlock entryBlock = findBlockContaining(cfg, entry); + BasicBlock branchBlock = findBlockContaining(cfg, branchInsn); + BasicBlock trueBlock = findBlockContaining(cfg, truePath); + BasicBlock falseBlock = findBlockContaining(cfg, falsePath); + BasicBlock exitBlock = cfg.vertexSet().stream() + .filter(BasicBlock::isExitBlock) + .findFirst() + .orElseThrow(() -> new AssertionError("Exit block not found")); + + Branch branch = BranchPool.getInstance(TEST_LOADER).getBranchForInstruction(branchInsn); + + return new GraphFixture(cfg, branchBlock, trueBlock, falseBlock, exitBlock, branch, entryBlock); + } + + private static final class TestRawControlFlowGraph extends RawControlFlowGraph { + private TestRawControlFlowGraph() { + super(TEST_LOADER, CLASS_NAME, METHOD_NAME, Opcodes.ACC_PUBLIC); + } + + private void connect(BytecodeInstruction src, BytecodeInstruction dst) { + super.addEdge(src, dst, false); + } + } + + private static BasicBlock findBlockContaining(ActualControlFlowGraph cfg, BytecodeInstruction instruction) { + return cfg.vertexSet().stream() + .filter(bb -> bb.containsInstruction(instruction)) + .findFirst() + .orElseThrow(() -> new AssertionError("Block containing instruction not found")); + } + + private static BytecodeInstruction instruction(int id, int opcode) { + BytecodeInstruction instruction = new BytecodeInstruction( + TEST_LOADER, + CLASS_NAME, + METHOD_NAME, + id, + id, + new InsnNode(opcode)); + instruction.setLineNumber(100 + id); + return instruction; + } + + private static BytecodeInstruction branchInstruction(int id) { + BytecodeInstruction instruction = new BytecodeInstruction( + TEST_LOADER, + CLASS_NAME, + METHOD_NAME, + id, + id, + new JumpInsnNode(Opcodes.IFEQ, new LabelNode())); + instruction.setLineNumber(200 + id); + BranchPool.getInstance(TEST_LOADER).registerAsBranch(instruction); + return instruction; + } + } +} + + diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cdg/DominatorNodeTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cdg/DominatorNodeTest.java new file mode 100644 index 0000000000..cb32a65584 --- /dev/null +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cdg/DominatorNodeTest.java @@ -0,0 +1,85 @@ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cdg; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class DominatorNodeTest { + + @Test + void constructorSetsNodeAndLabel() { + DominatorNode node = new DominatorNode<>("entry"); + assertEquals("entry", node.node); + assertSame(node, node.label); + } + + @Test + void linkSetsAncestor() { + DominatorNode node = new DominatorNode<>("entry"); + DominatorNode ancestor = new DominatorNode<>("ancestor"); + node.link(ancestor); + assertSame(ancestor, node.ancestor); + } + + @Test + void evalReturnsSelfWhenAncestorIsNull() { + DominatorNode node = new DominatorNode<>("entry"); + assertSame(node, node.eval()); + } + + @Test + void evalCompressesAncestorWhenPresentAndReturnsUpdatedLabel() { + DominatorNode node = new DominatorNode<>("node"); + DominatorNode ancestor = new DominatorNode<>("ancestor"); + DominatorNode ancestorAncestor = new DominatorNode<>("ancestorAncestor"); + + // In the Lengauer-Tarjan algorithm used to create the DominatorTree, every visited node has a N value + // N is the order of discovery of the node in the DFS of the DominatorTree + // Smaller N means its higher in the tree (closer to the root) + // Label always points to the node's best dominator candidate so far (lowest N value) + + // From every path on the start to the node, the node's semi-dominator is the node with the lowest N value + node.semiDominator = new DominatorNode<>("nodeSemi"); + node.semiDominator.n = 10; + + ancestor.semiDominator = new DominatorNode<>("ancestorSemi"); + ancestor.semiDominator.n = 1; + + // Link the nodes together to form a tree + node.link(ancestor); + ancestor.link(ancestorAncestor); + + // Evaluate the node to find its immediate dominator + DominatorNode result = node.eval(); + + // eval must return the best candidate, in this case the ancestor + assertSame(ancestor, result); + // label must be updated to the best candidate + assertSame(ancestor, node.label); + // compression also flattens the ancestor chain, so node.ancestor now points + // directly to the ancestor's ancestor + assertSame(ancestorAncestor, node.ancestor); + } + + @Test + void isRootNodeReturnsTrueWhenNIsOne() { + DominatorNode node = new DominatorNode<>("entry"); + node.n = 1; + assertTrue(node.isRootNode()); + } + + @Test + void isRootNodeReturnsFalseWhenNGreaterThanOne() { + DominatorNode node = new DominatorNode<>("entry"); + node.n = 2; + assertFalse(node.isRootNode()); + } + + @Test + void getFromBucketReturnsFirstElement() { + DominatorNode node = new DominatorNode<>("A"); + DominatorNode bucketNode = new DominatorNode<>("B"); + node.bucket.add(bucketNode); + assertSame(bucketNode, node.getFromBucket()); + } +} diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/ActualControlFlowGraphTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/ActualControlFlowGraphTest.java new file mode 100644 index 0000000000..0664bf04be --- /dev/null +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/ActualControlFlowGraphTest.java @@ -0,0 +1,168 @@ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg; + +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.branch.BranchPool; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.JumpInsnNode; +import org.objectweb.asm.tree.LabelNode; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +class ActualControlFlowGraphTest { + + private static final ClassLoader TEST_LOADER = ActualControlFlowGraphTest.class.getClassLoader(); + private static final String CLASS_NAME = "com.example.ActualCFGFixture"; + + @AfterEach + void resetPools() { + BranchPool.resetForTesting(TEST_LOADER); + } + + @Test + void constructorBuildsBasicBlocksAndBranches() { + GraphFixture fixture = GraphFixture.build("constructor"); + ActualControlFlowGraph cfg = new ActualControlFlowGraph(fixture.raw); + + BasicBlock entryBlock = cfg.vertexSet().stream() + .filter(BasicBlock::isEntryBlock) + .findFirst() + .orElseThrow(() -> new AssertionError("Synthetic entry block missing")); + BasicBlock exitBlock = cfg.vertexSet().stream() + .filter(BasicBlock::isExitBlock) + .findFirst() + .orElseThrow(() -> new AssertionError("Synthetic exit block missing")); + + assertNotNull(entryBlock); + assertNotNull(exitBlock); + + BasicBlock branchBlock = cfg.getBlockOf(fixture.branchInsn); + assertNotNull(branchBlock); + + BasicBlock trueBlock = cfg.getBlockOf(fixture.trueInsn); + BasicBlock falseBlock = cfg.getBlockOf(fixture.falseInsn); + + Set branchChildren = cfg.getChildren(branchBlock); + assertEquals(2, branchChildren.size()); + assertTrue(branchChildren.contains(trueBlock)); + assertTrue(branchChildren.contains(falseBlock)); + + assertEquals(1, cfg.getBranches().size()); + assertTrue(cfg.getBranches().contains(fixture.branchInsn)); + } + + @Test + void computeReverseCFGPreservesVertexCount() { + GraphFixture fixture = GraphFixture.build("reverse"); + ActualControlFlowGraph cfg = new ActualControlFlowGraph(fixture.raw); + + ActualControlFlowGraph reverse = cfg.computeReverseCFG(); + + assertEquals(cfg.vertexCount(), reverse.vertexCount()); + assertNotNull(reverse.getBlockOf(fixture.branchInsn)); + } + + @Test + void getBlockOfCachesBlocksAndGetInstructionUsesPool() { + GraphFixture fixture = GraphFixture.build("lookup"); + ActualControlFlowGraph cfg = new ActualControlFlowGraph(fixture.raw); + + BytecodeInstruction target = fixture.trueInsn; + BasicBlock first = cfg.getBlockOf(target); + assertNotNull(first); + BasicBlock second = cfg.getBlockOf(target); + assertSame(first, second); + + BytecodeInstruction resolved = cfg.getInstruction(target.getInstructionId()); + assertSame(target, resolved); + } + + private static final class GraphFixture { + final RawControlFlowGraph raw; + final BytecodeInstruction branchInsn; + final BytecodeInstruction trueInsn; + final BytecodeInstruction falseInsn; + + private GraphFixture(RawControlFlowGraph raw, + BytecodeInstruction branchInsn, + BytecodeInstruction trueInsn, + BytecodeInstruction falseInsn) { + this.raw = raw; + this.branchInsn = branchInsn; + this.trueInsn = trueInsn; + this.falseInsn = falseInsn; + } + + static GraphFixture build(String methodSuffix) { + String methodName = METHOD_NAME(methodSuffix); + TestRawCFG raw = new TestRawCFG(methodName); + + BytecodeInstruction entry = instruction(methodName, 0, Opcodes.NOP); + BytecodeInstruction branch = branchInstruction(methodName, 1); + BytecodeInstruction truePath = instruction(methodName, 2, Opcodes.NOP); + BytecodeInstruction falsePath = instruction(methodName, 3, Opcodes.NOP); + BytecodeInstruction exit = instruction(methodName, 4, Opcodes.RETURN); + + raw.addVertex(entry); + raw.addVertex(branch); + raw.addVertex(truePath); + raw.addVertex(falsePath); + raw.addVertex(exit); + + raw.connect(entry, branch); + raw.connect(branch, truePath); + raw.connect(branch, falsePath); + raw.connect(truePath, exit); + raw.connect(falsePath, exit); + + return new GraphFixture(raw, branch, truePath, falsePath); + } + + private static String METHOD_NAME(String suffix) { + return "sample_" + suffix + "()V"; + } + } + + private static BytecodeInstruction instruction(String methodName, int instructionId, int opcode) { + BytecodeInstruction instruction = new BytecodeInstruction( + TEST_LOADER, + CLASS_NAME, + methodName, + instructionId, + instructionId, + new InsnNode(opcode) + ); + instruction.setLineNumber(100 + instructionId); + BytecodeInstructionPool.getInstance(TEST_LOADER).registerInstruction(instruction); + return instruction; + } + + private static BytecodeInstruction branchInstruction(String methodName, int instructionId) { + BytecodeInstruction instruction = new BytecodeInstruction( + TEST_LOADER, + CLASS_NAME, + methodName, + instructionId, + instructionId, + new JumpInsnNode(Opcodes.IFEQ, new LabelNode()) + ); + instruction.setLineNumber(200 + instructionId); + BytecodeInstructionPool.getInstance(TEST_LOADER).registerInstruction(instruction); + BranchPool.getInstance(TEST_LOADER).registerAsBranch(instruction); + return instruction; + } + + private static final class TestRawCFG extends RawControlFlowGraph { + private TestRawCFG(String methodName) { + super(TEST_LOADER, CLASS_NAME, methodName, Opcodes.ACC_PUBLIC); + } + + private void connect(BytecodeInstruction src, BytecodeInstruction dst) { + super.addEdge(src, dst, false); + } + } +} + diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/BasicBlockTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/BasicBlockTest.java new file mode 100644 index 0000000000..e79cd12f50 --- /dev/null +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/BasicBlockTest.java @@ -0,0 +1,105 @@ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg; + +import org.junit.jupiter.api.Test; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.InsnNode; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class BasicBlockTest { + + private static final ClassLoader TEST_LOADER = BasicBlockTest.class.getClassLoader(); + private static final String CLASS_NAME = "com.example.BasicBlockFixture"; + private static final String METHOD_NAME = "sample()V"; + + @Test + void constructorRejectsNullArguments() { + BytecodeInstruction instruction = instruction(CLASS_NAME, METHOD_NAME, 0, 100); + List nodes = Collections.singletonList(instruction); + + assertThrows(IllegalArgumentException.class, + () -> new BasicBlock(TEST_LOADER, null, METHOD_NAME, nodes)); + assertThrows(IllegalArgumentException.class, + () -> new BasicBlock(TEST_LOADER, CLASS_NAME, null, nodes)); + assertThrows(IllegalArgumentException.class, + () -> new BasicBlock(TEST_LOADER, CLASS_NAME, METHOD_NAME, null)); + } + + @Test + void constructorRejectsInvalidInstructionLists() { + BytecodeInstruction valid = instruction(CLASS_NAME, METHOD_NAME, 1, 101); + BytecodeInstruction otherClass = instruction("other.Class", METHOD_NAME, 2, 102); + BytecodeInstruction otherMethod = instruction(CLASS_NAME, "other()V", 3, 103); + BytecodeInstruction duplicate = instruction(CLASS_NAME, METHOD_NAME, 4, 104); + + assertThrows(IllegalArgumentException.class, + () -> new BasicBlock(TEST_LOADER, CLASS_NAME, METHOD_NAME, Arrays.asList(valid, otherClass))); + assertThrows(IllegalArgumentException.class, + () -> new BasicBlock(TEST_LOADER, CLASS_NAME, METHOD_NAME, Arrays.asList(valid, otherMethod))); + assertThrows(IllegalArgumentException.class, + () -> new BasicBlock(TEST_LOADER, CLASS_NAME, METHOD_NAME, Arrays.asList(duplicate, duplicate))); + } + + @Test + void constructorRejectsInstructionsAlreadyBelongingToAnotherBlock() { + BytecodeInstruction shared = instruction(CLASS_NAME, METHOD_NAME, 5, 105); + new BasicBlock(TEST_LOADER, CLASS_NAME, METHOD_NAME, Collections.singletonList(shared)); + + assertThrows(IllegalArgumentException.class, + () -> new BasicBlock(TEST_LOADER, CLASS_NAME, METHOD_NAME, Collections.singletonList(shared))); + } + + @Test + void exposesInstructionOrderLinesAndFlags() { + BytecodeInstruction first = instruction(CLASS_NAME, METHOD_NAME, 6, 106); + BytecodeInstruction second = instruction(CLASS_NAME, METHOD_NAME, 7, 207); + BasicBlock block = new BasicBlock(TEST_LOADER, CLASS_NAME, METHOD_NAME, Arrays.asList(first, second)); + + assertSame(first, block.getFirstInstruction()); + assertSame(second, block.getLastInstruction()); + assertEquals(106, block.getFirstLine()); + assertEquals(207, block.getLastLine()); + assertTrue(block.containsInstruction(first)); + assertTrue(block.containsInstruction(second)); + + List iterated = new ArrayList<>(); + block.forEach(iterated::add); + assertEquals(Arrays.asList(first, second), iterated); + + assertFalse(block.isEntryBlock()); + assertFalse(block.isExitBlock()); + } + + @Test + void nameAndToStringReflectBlockIdentity() { + BytecodeInstruction ins = instruction(CLASS_NAME, METHOD_NAME, 8, 208); + BasicBlock block = new BasicBlock(TEST_LOADER, CLASS_NAME, METHOD_NAME, Collections.singletonList(ins)); + + assertTrue(block.getName().contains("BasicBlock")); + assertTrue(block.toString().contains(ins.getInstructionType())); + assertTrue(block.toString().contains("l208")); + + BasicBlock other = new BasicBlock(TEST_LOADER, CLASS_NAME, METHOD_NAME, Collections.singletonList(instruction(CLASS_NAME, METHOD_NAME, 9, 209))); + assertNotEquals(block.getName(), other.getName()); + assertNotEquals(block, other); + } + + private static BytecodeInstruction instruction(String className, String methodName, int id, int line) { + BytecodeInstruction instruction = new BytecodeInstruction( + TEST_LOADER, + className, + methodName, + id, + id, + new InsnNode(Opcodes.NOP) + ); + instruction.setLineNumber(line); + return instruction; + } +} + diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/BytecodeInstructionPoolTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/BytecodeInstructionPoolTest.java new file mode 100644 index 0000000000..dfd2f43a89 --- /dev/null +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/BytecodeInstructionPoolTest.java @@ -0,0 +1,89 @@ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg; + +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.branch.BranchPool; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.*; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class BytecodeInstructionPoolTest { + + private static final ClassLoader TEST_LOADER = BytecodeInstructionPoolTest.class.getClassLoader(); + private static final String CLASS_NAME_PREFIX = "com.example.BytecodeInstructionPoolFixture"; + private static final String METHOD_NAME_PREFIX = "sample"; + + @AfterEach + void resetBranchPool() { + BranchPool.resetForTesting(TEST_LOADER); + } + + @Test + void registerMethodNodePopulatesMappingsAndBranches() { + String className = uniqueClassName("register"); + String methodName = uniqueMethodName("register"); + BytecodeInstructionPool pool = BytecodeInstructionPool.getInstance(TEST_LOADER); + MethodNode methodNode = new MethodNode(Opcodes.ASM9, Opcodes.ACC_PUBLIC, methodName, "()V", null, null); + + LabelNode label = new LabelNode(); + methodNode.instructions.add(new LineNumberNode(100, label)); + methodNode.instructions.add(label); + methodNode.instructions.add(new InsnNode(Opcodes.NOP)); + methodNode.instructions.add(new JumpInsnNode(Opcodes.IFEQ, new LabelNode())); + methodNode.instructions.add(new InsnNode(Opcodes.RETURN)); + + List instructions = pool.registerMethodNode(methodNode, className, methodName); + + assertEquals(5, instructions.size()); + assertTrue(instructions.get(2).hasLineNumberSet()); + assertEquals(100, instructions.get(2).getLineNumber()); + + BytecodeInstruction branch = instructions.get(3); + assertTrue(branch.isBranch()); + assertTrue(BranchPool.getInstance(TEST_LOADER).isKnownAsBranch(branch)); + + List byLookup = pool.getInstructionsIn(className, methodName); + assertEquals(instructions, byLookup); + } + + @Test + void getInstructionByIdReturnsRegisteredInstruction() { + String className = uniqueClassName("lookup"); + String methodName = uniqueMethodName("lookup"); + BytecodeInstructionPool pool = BytecodeInstructionPool.getInstance(TEST_LOADER); + MethodNode methodNode = new MethodNode(Opcodes.ASM9, Opcodes.ACC_PUBLIC, methodName, "()V", null, null); + methodNode.instructions.add(new InsnNode(Opcodes.NOP)); + methodNode.instructions.add(new InsnNode(Opcodes.RETURN)); + pool.registerMethodNode(methodNode, className, methodName); + + BytecodeInstruction instruction = pool.getInstruction(className, methodName, 1); + assertNotNull(instruction); + assertEquals(Opcodes.RETURN, instruction.getASMNode().getOpcode()); + } + + @Test + void forgetInstructionRemovesFromPool() { + String className = uniqueClassName("forget"); + String methodName = uniqueMethodName("forget"); + BytecodeInstructionPool pool = BytecodeInstructionPool.getInstance(TEST_LOADER); + MethodNode methodNode = new MethodNode(Opcodes.ASM9, Opcodes.ACC_PUBLIC, methodName, "()V", null, null); + methodNode.instructions.add(new InsnNode(Opcodes.NOP)); + pool.registerMethodNode(methodNode, className, methodName); + + BytecodeInstruction instruction = pool.getInstruction(className, methodName, 0); + assertTrue(pool.forgetInstruction(instruction)); + assertTrue(pool.getInstructionsIn(className, methodName).isEmpty()); + } + + private static String uniqueClassName(String suffix) { + return CLASS_NAME_PREFIX + "." + suffix + "." + System.nanoTime(); + } + + private static String uniqueMethodName(String suffix) { + return METHOD_NAME_PREFIX + "_" + suffix + "_" + System.nanoTime(); + } +} + diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/BytecodeInstructionTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/BytecodeInstructionTest.java new file mode 100644 index 0000000000..ade368d33b --- /dev/null +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/BytecodeInstructionTest.java @@ -0,0 +1,132 @@ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg; + +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.branch.Branch; +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.branch.BranchPool; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.JumpInsnNode; +import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.LineNumberNode; + +import static org.junit.jupiter.api.Assertions.*; + +class BytecodeInstructionTest { + + private static final ClassLoader TEST_LOADER = BytecodeInstructionTest.class.getClassLoader(); + private static final String CLASS_NAME = "com.example.BytecodeInstructionFixture"; + private static final String METHOD_NAME = "sample()V"; + + @AfterEach + void resetBranchPool() { + BranchPool.resetForTesting(TEST_LOADER); + } + + @Test + void constructorRejectsInvalidArguments() { + AbstractInsnNodeStub node = new AbstractInsnNodeStub(Opcodes.NOP); + assertThrows(IllegalArgumentException.class, + () -> new BytecodeInstruction(TEST_LOADER, null, METHOD_NAME, 0, 0, node)); + assertThrows(IllegalArgumentException.class, + () -> new BytecodeInstruction(TEST_LOADER, CLASS_NAME, METHOD_NAME, -1, 0, node)); + assertThrows(IllegalArgumentException.class, + () -> new BytecodeInstruction(TEST_LOADER, CLASS_NAME, METHOD_NAME, 0, 0, null)); + } + + @Test + void setBasicBlockValidatesClassAndSingleAssignment() { + BytecodeInstruction instruction = instruction(1, new InsnNode(Opcodes.NOP)); + + DummyBlock matching = new DummyBlock(CLASS_NAME, METHOD_NAME); + instruction.setBasicBlock(matching); + assertTrue(instruction.hasBasicBlockSet()); + assertSame(matching, instruction.getBasicBlock()); + + DummyBlock wrongClass = new DummyBlock("other.Class", METHOD_NAME); + assertThrows(IllegalArgumentException.class, () -> instruction.setBasicBlock(wrongClass)); + assertThrows(IllegalArgumentException.class, () -> instruction.setBasicBlock(matching)); + } + + @Test + void lineNumberManagementHandlesManualAndAsmValues() { + BytecodeInstruction manual = instruction(2, new InsnNode(Opcodes.NOP)); + manual.setLineNumber(150); + assertEquals(150, manual.getLineNumber()); + + BytecodeInstruction fromAsm = new BytecodeInstruction( + TEST_LOADER, + CLASS_NAME, + METHOD_NAME, + 3, + 3, + new LineNumberNode(321, new LabelNode()) + ); + assertEquals(321, fromAsm.getLineNumber()); + } + + @Test + void branchDetectionAndExplainUseBranchPoolInformation() { + BytecodeInstruction branchInsn = instruction(4, new JumpInsnNode(Opcodes.IFEQ, new LabelNode())); + BranchPool.getInstance(TEST_LOADER).registerAsBranch(branchInsn); + + assertTrue(branchInsn.isBranch()); + Branch branch = branchInsn.toBranch(); + assertNotNull(branch); + + String explanation = branchInsn.explain(); + assertTrue(explanation.contains("Branch")); + assertTrue(explanation.contains("IFEQ")); + } + + @Test + void returnAndThrowDetectionWorks() { + BytecodeInstruction returnInsn = instruction(5, new InsnNode(Opcodes.RETURN)); + assertTrue(returnInsn.isReturn()); + assertTrue(returnInsn.canReturnFromMethod()); + assertFalse(returnInsn.isThrow()); + + BytecodeInstruction throwInsn = instruction(6, new InsnNode(Opcodes.ATHROW)); + assertFalse(throwInsn.isReturn()); + assertTrue(throwInsn.isThrow()); + assertTrue(throwInsn.canReturnFromMethod()); + } + + private static BytecodeInstruction instruction(int id, org.objectweb.asm.tree.AbstractInsnNode node) { + return new BytecodeInstruction( + TEST_LOADER, + CLASS_NAME, + METHOD_NAME, + id, + id, + node + ); + } + + private static final class DummyBlock extends BasicBlock { + private DummyBlock(String className, String methodName) { + super(className, methodName); + } + } + + private static final class AbstractInsnNodeStub extends org.objectweb.asm.tree.AbstractInsnNode { + private AbstractInsnNodeStub(int opcode) { + super(opcode); + } + + @Override + public int getType() { + return 0; + } + + @Override + public void accept(org.objectweb.asm.MethodVisitor methodVisitor) { + } + + @Override + public org.objectweb.asm.tree.AbstractInsnNode clone(java.util.Map labels) { + return new AbstractInsnNodeStub(opcode); + } + } +} + diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/CFGGeneratorTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/CFGGeneratorTest.java new file mode 100644 index 0000000000..330bbfa3e2 --- /dev/null +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/CFGGeneratorTest.java @@ -0,0 +1,114 @@ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg; + +import org.evomaster.client.java.instrumentation.dynamosa.graphs.GraphPool; +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.branch.BranchPool; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.LineNumberNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.analysis.Frame; + +import static org.junit.jupiter.api.Assertions.*; + +class CFGGeneratorTest { + + private static final ClassLoader TEST_LOADER = CFGGeneratorTest.class.getClassLoader(); + private static final String CLASS_NAME_PREFIX = "com.example.CFGGeneratorFixture"; + private static final String METHOD_NAME_PREFIX = "sample"; + + @AfterEach + void resetPools() { + GraphPool.resetForTesting(TEST_LOADER); + BranchPool.resetForTesting(TEST_LOADER); + } + + @Test + void constructorRejectsNullArguments() { + MethodNode method = simpleMethodNode(); + assertThrows(IllegalArgumentException.class, () -> new CFGGenerator(TEST_LOADER, null, "m()V", method)); + assertThrows(IllegalArgumentException.class, () -> new CFGGenerator(TEST_LOADER, "c", null, method)); + assertThrows(IllegalArgumentException.class, () -> new CFGGenerator(TEST_LOADER, "c", "m()V", null)); + } + + @Test + void registerCFGsStoresGraphsInPool() { + String className = uniqueClassName("store"); + String methodName = uniqueMethodName("store"); + MethodNode method = simpleMethodNode(); + + CFGGenerator generator = new CFGGenerator(TEST_LOADER, className, methodName, method); + + // Frames in ASM are analysis snapshots of the JVM state at each instruction. + // We don't care about them, but registerControlFlowEdge() requires them. + // So, we create a dummy frame for each instruction. + + int size = method.instructions.size(); + Frame[] frames = new Frame[size]; + for (int i = 0; i < size; i++) { + frames[i] = new Frame<>(0, 0); + } + for (int i = 0; i < size - 1; i++) { + generator.registerControlFlowEdge(i, i + 1, frames, false); + } + + generator.registerCFGs(); + + RawControlFlowGraph raw = GraphPool.getInstance(TEST_LOADER).getRawCFG(className, methodName); + ActualControlFlowGraph actual = GraphPool.getInstance(TEST_LOADER).getActualCFG(className, methodName); + + assertSame(generator.getRawGraph(), raw); + assertNotNull(actual); + assertFalse(raw.vertexSet().isEmpty()); + assertFalse(actual.vertexSet().isEmpty()); + } + + @Test + void registerControlFlowEdgeRejectsNullFrames() { + String className = uniqueClassName("nullFrames"); + String methodName = uniqueMethodName("nullFrames"); + MethodNode method = simpleMethodNode(); + + CFGGenerator generator = new CFGGenerator(TEST_LOADER, className, methodName, method); + + assertThrows(IllegalArgumentException.class, () -> + generator.registerControlFlowEdge(0, 1, null, false)); + } + + @Test + void registerControlFlowEdgeSkipsUnreachableDestination() { + String className = uniqueClassName("unreachable"); + String methodName = uniqueMethodName("unreachable"); + MethodNode method = simpleMethodNode(); + + CFGGenerator generator = new CFGGenerator(TEST_LOADER, className, methodName, method); + + Frame[] frames = new Frame[2]; + generator.registerControlFlowEdge(0, 1, frames, false); + + assertTrue(generator.getRawGraph().edgeSet().isEmpty()); + } + + private static MethodNode simpleMethodNode() { + MethodNode methodNode = new MethodNode(Opcodes.ASM9, Opcodes.ACC_PUBLIC, "dummy", "()V", null, null); + + LabelNode label = new LabelNode(); + methodNode.instructions.add(new LineNumberNode(100, label)); + methodNode.instructions.add(label); + methodNode.instructions.add(new InsnNode(Opcodes.NOP)); + methodNode.instructions.add(new InsnNode(Opcodes.RETURN)); + + return methodNode; + } + + private static String uniqueClassName(String suffix) { + return CLASS_NAME_PREFIX + "." + suffix + "." + System.nanoTime(); + } + + private static String uniqueMethodName(String suffix) { + return METHOD_NAME_PREFIX + "_" + suffix + "_" + System.nanoTime() + "()V"; + } +} + diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/ControlDependencyTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/ControlDependencyTest.java new file mode 100644 index 0000000000..be56e490c3 --- /dev/null +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/ControlDependencyTest.java @@ -0,0 +1,68 @@ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg; + +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.branch.Branch; +import org.junit.jupiter.api.Test; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.JumpInsnNode; +import org.objectweb.asm.tree.LabelNode; + +import static org.junit.jupiter.api.Assertions.*; + +class ControlDependencyTest { + + private static final ClassLoader TEST_LOADER = ControlDependencyTest.class.getClassLoader(); + private static final String CLASS_NAME = "com.example.ControlDependencyFixture"; + private static final String METHOD_NAME = "sample()V"; + + @Test + void constructorRequiresBranch() { + assertThrows(IllegalArgumentException.class, () -> new ControlDependency(null, true)); + } + + @Test + void gettersReflectBranchAndExpressionValue() { + Branch branch = branch(1, 120); + ControlDependency dependency = new ControlDependency(branch, true); + + assertSame(branch, dependency.getBranch()); + assertTrue(dependency.getBranchExpressionValue()); + assertTrue(dependency.toString().contains("TRUE")); + + ControlDependency falseDependency = new ControlDependency(branch, false); + assertFalse(falseDependency.getBranchExpressionValue()); + assertTrue(falseDependency.toString().contains("FALSE")); + } + + @Test + void equalsAndHashCodeDependOnBranchAndValue() { + Branch branchA = branch(2, 130); + Branch branchB = branch(3, 140); + + ControlDependency depTrue = new ControlDependency(branchA, true); + ControlDependency depTrueCopy = new ControlDependency(branchA, true); + ControlDependency depFalse = new ControlDependency(branchA, false); + ControlDependency depOtherBranch = new ControlDependency(branchB, true); + + assertEquals(depTrue, depTrueCopy); + assertEquals(depTrue.hashCode(), depTrueCopy.hashCode()); + + assertNotEquals(depTrue, depFalse); + assertNotEquals(depTrue, depOtherBranch); + assertNotEquals(depTrue, null); + assertNotEquals(depTrue, "dependency"); + } + + private static Branch branch(int instructionId, int lineNumber) { + BytecodeInstruction instruction = new BytecodeInstruction( + TEST_LOADER, + CLASS_NAME, + METHOD_NAME, + instructionId, + instructionId, + new JumpInsnNode(Opcodes.IFEQ, new LabelNode()) + ); + instruction.setLineNumber(lineNumber); + return new Branch(instruction, instructionId + 1); + } +} + diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/ControlFlowEdgeTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/ControlFlowEdgeTest.java new file mode 100644 index 0000000000..558a9d435a --- /dev/null +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/ControlFlowEdgeTest.java @@ -0,0 +1,76 @@ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg; + +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.branch.Branch; +import org.junit.jupiter.api.Test; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.JumpInsnNode; +import org.objectweb.asm.tree.LabelNode; + +import static org.junit.jupiter.api.Assertions.*; + +class ControlFlowEdgeTest { + + private static final ClassLoader TEST_LOADER = ControlFlowEdgeTest.class.getClassLoader(); + private static final String CLASS_NAME = "com.example.ControlFlowEdgeFixture"; + private static final String METHOD_NAME = "sample()V"; + + @Test + void defaultConstructorProducesEdgeWithoutDependency() { + ControlFlowEdge edge = new ControlFlowEdge(); + + assertFalse(edge.hasControlDependency()); + assertNull(edge.getControlDependency()); + assertNull(edge.getBranchInstruction()); + assertTrue(edge.getBranchExpressionValue()); + assertFalse(edge.isExceptionEdge()); + } + + @Test + void constructorWithDependencyStoresBranchAndExceptionFlag() { + Branch branch = branch(1, 120); + ControlDependency dependency = new ControlDependency(branch, false); + + ControlFlowEdge edge = new ControlFlowEdge(dependency, true); + + assertTrue(edge.hasControlDependency()); + assertSame(dependency, edge.getControlDependency()); + assertSame(branch, edge.getBranchInstruction()); + assertFalse(edge.getBranchExpressionValue()); + assertTrue(edge.isExceptionEdge()); + assertTrue(edge.toString().contains("FALSE")); + } + + @Test + void copyConstructorDuplicatesFlagsAndDependencyReference() { + ControlFlowEdge original = new ControlFlowEdge(new ControlDependency(branch(2, 130), true), false); + + ControlFlowEdge copy = new ControlFlowEdge(original); + + assertNotSame(original, copy); + assertEquals(original.hasControlDependency(), copy.hasControlDependency()); + assertSame(original.getControlDependency(), copy.getControlDependency()); + assertEquals(original.isExceptionEdge(), copy.isExceptionEdge()); + } + + @Test + void booleanConstructorOnlySetsExceptionFlag() { + ControlFlowEdge edge = new ControlFlowEdge(true); + + assertTrue(edge.isExceptionEdge()); + assertFalse(edge.hasControlDependency()); + } + + private static Branch branch(int instructionId, int lineNumber) { + BytecodeInstruction instruction = new BytecodeInstruction( + TEST_LOADER, + CLASS_NAME, + METHOD_NAME, + instructionId, + instructionId, + new JumpInsnNode(Opcodes.IFEQ, new LabelNode()) + ); + instruction.setLineNumber(lineNumber); + return new Branch(instruction, instructionId + 10); + } +} + diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/EntryBlockTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/EntryBlockTest.java new file mode 100644 index 0000000000..cdb229be72 --- /dev/null +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/EntryBlockTest.java @@ -0,0 +1,31 @@ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class EntryBlockTest { + + private static final String CLASS_NAME = "com.example.EntryBlockFixture"; + private static final String METHOD_NAME = "sample()V"; + + @Test + void entryBlockSetsFlagsAndMetadata() { + EntryBlock block = new EntryBlock(CLASS_NAME, METHOD_NAME); + + assertTrue(block.isEntryBlock()); + assertFalse(block.isExitBlock()); + assertEquals(CLASS_NAME, block.getClassName()); + assertEquals(METHOD_NAME, block.getMethodName()); + } + + @Test + void entryBlockProvidesDescriptiveNameAndToString() { + EntryBlock block = new EntryBlock(CLASS_NAME, METHOD_NAME); + + String expected = "EntryBlock for method " + METHOD_NAME; + assertEquals(expected, block.getName()); + assertEquals(expected, block.toString()); + } +} + diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/ExitBlockTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/ExitBlockTest.java new file mode 100644 index 0000000000..8da5ade2af --- /dev/null +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/ExitBlockTest.java @@ -0,0 +1,31 @@ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ExitBlockTest { + + private static final String CLASS_NAME = "com.example.ExitBlockFixture"; + private static final String METHOD_NAME = "sample()V"; + + @Test + void exitBlockSetsFlagsAndMetadata() { + ExitBlock block = new ExitBlock(CLASS_NAME, METHOD_NAME); + + assertTrue(block.isExitBlock()); + assertFalse(block.isEntryBlock()); + assertEquals(CLASS_NAME, block.getClassName()); + assertEquals(METHOD_NAME, block.getMethodName()); + } + + @Test + void exitBlockProvidesDescriptiveNameAndToString() { + ExitBlock block = new ExitBlock(CLASS_NAME, METHOD_NAME); + + String expected = "ExitBlock for method " + METHOD_NAME; + assertEquals(expected, block.getName()); + assertEquals(expected, block.toString()); + } +} + diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/RawControlFlowGraphTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/RawControlFlowGraphTest.java new file mode 100644 index 0000000000..efbbdfa5f3 --- /dev/null +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/RawControlFlowGraphTest.java @@ -0,0 +1,159 @@ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg; + +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.branch.BranchPool; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.JumpInsnNode; +import org.objectweb.asm.tree.LabelNode; + +import java.util.Arrays; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +class RawControlFlowGraphTest { + + private static final ClassLoader TEST_LOADER = RawControlFlowGraphTest.class.getClassLoader(); + private static final String CLASS_NAME = "com.example.RawCFGFixture"; + private static final String METHOD_NAME = "sample()V"; + + @AfterEach + void resetPools() { + BranchPool.resetForTesting(TEST_LOADER); + } + + @Test + void addEdgeCreatesControlDependenciesForBranchEdges() { + TestRawCFG cfg = new TestRawCFG(); + + BytecodeInstruction branch = branchInstruction(10); + BytecodeInstruction jumpTarget = instruction(30, Opcodes.NOP); + BytecodeInstruction fallThrough = instruction(11, Opcodes.NOP); + + cfg.addVertex(branch); + cfg.addVertex(jumpTarget); + cfg.addVertex(fallThrough); + + ControlFlowEdge jumpEdge = cfg.connect(branch, jumpTarget); + ControlFlowEdge fallEdge = cfg.connect(branch, fallThrough); + + assertTrue(jumpEdge.hasControlDependency()); + assertTrue(jumpEdge.getControlDependency().getBranchExpressionValue()); + assertFalse(fallEdge.getControlDependency().getBranchExpressionValue()); + assertSame(branch.toBranch(), jumpEdge.getBranchInstruction()); + } + + @Test + void determineEntryAndExitPointsFallbackToMinAndMaxWhenNecessary() { + TestRawCFG cfg = new TestRawCFG(); + BytecodeInstruction first = instruction(0, Opcodes.NOP); + BytecodeInstruction second = instruction(1, Opcodes.NOP); + + cfg.addVertex(first); + cfg.addVertex(second); + + cfg.connect(first, second); + cfg.connect(second, first); // cycle -> no node with zero in-degree/out-degree + + assertSame(first, cfg.determineEntryPoint()); + + Set exits = cfg.determineExitPoints(); + assertEquals(1, exits.size()); + assertTrue(exits.contains(second)); + } + + @Test + void determineBranchesAndJoinsReflectOutAndInDegrees() { + TestRawCFG cfg = new TestRawCFG(); + BytecodeInstruction entry = instruction(0, Opcodes.NOP); + BytecodeInstruction branch = branchInstruction(5); + BytecodeInstruction truePath = instruction(6, Opcodes.NOP); + BytecodeInstruction falsePath = instruction(7, Opcodes.NOP); + BytecodeInstruction join = instruction(8, Opcodes.NOP); + + Arrays.asList(entry, branch, truePath, falsePath, join) + .forEach(cfg::addVertex); + + cfg.connect(entry, branch); + cfg.connect(branch, truePath); + cfg.connect(branch, falsePath); + cfg.connect(truePath, join); + cfg.connect(falsePath, join); + + assertEquals( + java.util.Collections.singleton(branch), + cfg.determineBranches() + ); + + assertEquals( + java.util.Collections.singleton(join), + cfg.determineJoins() + ); + } + + @Test + void determineBasicBlockMergesLinearSequence() { + TestRawCFG cfg = new TestRawCFG(); + BytecodeInstruction first = instruction(0, Opcodes.NOP); + BytecodeInstruction middle = instruction(1, Opcodes.NOP); + BytecodeInstruction last = instruction(2, Opcodes.RETURN); + + cfg.addVertex(first); + cfg.addVertex(middle); + cfg.addVertex(last); + + cfg.connect(first, middle); + cfg.connect(middle, last); + + BasicBlock block = cfg.determineBasicBlockFor(middle); + + assertEquals(first, block.getFirstInstruction()); + assertEquals(last, block.getLastInstruction()); + java.util.List collected = new java.util.ArrayList<>(); + block.forEach(collected::add); + assertEquals( + Arrays.asList(first, middle, last), + collected + ); + } + + private static BytecodeInstruction instruction(int instructionId, int opcode) { + BytecodeInstruction instruction = new BytecodeInstruction( + TEST_LOADER, + CLASS_NAME, + METHOD_NAME, + instructionId, + instructionId, + new InsnNode(opcode) + ); + instruction.setLineNumber(100 + instructionId); + return instruction; + } + + private static BytecodeInstruction branchInstruction(int instructionId) { + BytecodeInstruction instruction = new BytecodeInstruction( + TEST_LOADER, + CLASS_NAME, + METHOD_NAME, + instructionId, + instructionId, + new JumpInsnNode(Opcodes.IFEQ, new LabelNode()) + ); + instruction.setLineNumber(200 + instructionId); + BranchPool.getInstance(TEST_LOADER).registerAsBranch(instruction); + return instruction; + } + + private static final class TestRawCFG extends RawControlFlowGraph { + private TestRawCFG() { + super(TEST_LOADER, CLASS_NAME, METHOD_NAME, Opcodes.ACC_PUBLIC); + } + + private ControlFlowEdge connect(BytecodeInstruction src, BytecodeInstruction dst) { + return super.addEdge(src, dst, false); + } + } +} + diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/branch/BranchPoolTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/branch/BranchPoolTest.java new file mode 100644 index 0000000000..1891790ec7 --- /dev/null +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/branch/BranchPoolTest.java @@ -0,0 +1,117 @@ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.branch; + +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.JumpInsnNode; +import org.objectweb.asm.tree.LabelNode; + +import static org.junit.jupiter.api.Assertions.*; + +class BranchPoolTest { + + private static final ClassLoader TEST_LOADER = BranchPoolTest.class.getClassLoader(); + private static final String CLASS_NAME = "com.example.BranchPoolFixture"; + private static final String METHOD_NAME = "sample()V"; + + @AfterEach + void resetPool() { + BranchPool.resetForTesting(TEST_LOADER); + } + + @Test + void registerAsBranchRejectsNonBranchInstructions() { + BranchPool pool = BranchPool.getInstance(TEST_LOADER); + BytecodeInstruction notBranch = nonBranchInstruction(1); + + assertThrows(IllegalArgumentException.class, () -> pool.registerAsBranch(notBranch)); + } + + @Test + void registerAsBranchAssignsDeterministicIdsAndObjectiveNamesCorrectly() { + BranchPool pool = BranchPool.getInstance(TEST_LOADER); + BytecodeInstruction first = branchInstruction(2, 150); + BytecodeInstruction second = branchInstruction(3, 150); // same line to exercise ordinal handling + + pool.registerAsBranch(first); + pool.registerAsBranch(second); + + Branch firstBranch = pool.getBranchForInstruction(first); + Branch secondBranch = pool.getBranchForInstruction(second); + + assertEquals(1, firstBranch.getActualBranchId()); + assertEquals(2, secondBranch.getActualBranchId()); + + assertEquals("Branch_at_com.example.BranchPoolFixture_at_line_00150_position_0_trueBranch_153", + firstBranch.getThenObjectiveId()); + assertEquals("Branch_at_com.example.BranchPoolFixture_at_line_00150_position_0_falseBranch_153", + firstBranch.getElseObjectiveId()); + assertEquals("Branch_at_com.example.BranchPoolFixture_at_line_00150_position_1_trueBranch_153", + secondBranch.getThenObjectiveId()); + assertEquals("Branch_at_com.example.BranchPoolFixture_at_line_00150_position_1_falseBranch_153", + secondBranch.getElseObjectiveId()); + } + + @Test + void isKnownAsBranchReflectsRegistrationState() { + BranchPool pool = BranchPool.getInstance(TEST_LOADER); + BytecodeInstruction instruction = branchInstruction(4, 200); + + assertFalse(pool.isKnownAsBranch(instruction)); + + pool.registerAsBranch(instruction); + + assertTrue(pool.isKnownAsBranch(instruction)); + } + + @Test + void getBranchForInstructionRejectsUnknownBranches() { + BranchPool pool = BranchPool.getInstance(TEST_LOADER); + BytecodeInstruction instruction = branchInstruction(5, 220); + + assertThrows(IllegalArgumentException.class, () -> pool.getBranchForInstruction(instruction)); + } + + @Test + void registeringSameInstructionTwiceReturnsExistingBranch() { + BranchPool pool = BranchPool.getInstance(TEST_LOADER); + BytecodeInstruction instruction = branchInstruction(6, 230); + + pool.registerAsBranch(instruction); + Branch first = pool.getBranchForInstruction(instruction); + + pool.registerAsBranch(instruction); + Branch second = pool.getBranchForInstruction(instruction); + + assertSame(first, second); + } + + private static BytecodeInstruction branchInstruction(int instructionId, int lineNumber) { + BytecodeInstruction instruction = new BytecodeInstruction( + TEST_LOADER, + CLASS_NAME, + METHOD_NAME, + instructionId, + instructionId, + new JumpInsnNode(Opcodes.IFEQ, new LabelNode()) + ); + instruction.setLineNumber(lineNumber); + return instruction; + } + + private static BytecodeInstruction nonBranchInstruction(int instructionId) { + BytecodeInstruction instruction = new BytecodeInstruction( + TEST_LOADER, + CLASS_NAME, + METHOD_NAME, + instructionId, + instructionId, + new InsnNode(Opcodes.NOP) + ); + instruction.setLineNumber(50 + instructionId); + return instruction; + } +} + diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/branch/BranchTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/branch/BranchTest.java new file mode 100644 index 0000000000..18dfebbf24 --- /dev/null +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/dynamosa/graphs/cfg/branch/BranchTest.java @@ -0,0 +1,105 @@ +package org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.branch; + +import org.evomaster.client.java.instrumentation.dynamosa.graphs.cfg.BytecodeInstruction; +import org.junit.jupiter.api.Test; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.JumpInsnNode; +import org.objectweb.asm.tree.LabelNode; + +import static org.junit.jupiter.api.Assertions.*; + +class BranchTest { + + private static final ClassLoader TEST_LOADER = BranchTest.class.getClassLoader(); + private static final String CLASS_NAME = "com.example.BranchFixture"; + private static final String METHOD_NAME = "sample()V"; + + @Test + void constructorRejectsNonBranchInstructions() { + BytecodeInstruction notBranch = nonBranchInstruction(1); + assertThrows(IllegalArgumentException.class, () -> new Branch(notBranch, 1)); + } + + @Test + void constructorRejectsNonPositiveIds() { + BytecodeInstruction instruction = branchInstruction(2); + assertThrows(IllegalStateException.class, () -> new Branch(instruction, 0)); + } + + @Test + void gettersExposeInstructionMetadata() { + BytecodeInstruction instruction = branchInstruction(3); + Branch branch = new Branch(instruction, 7); + + assertEquals(7, branch.getActualBranchId()); + assertSame(instruction, branch.getInstruction()); + assertEquals(CLASS_NAME, branch.getClassName()); + assertEquals(METHOD_NAME, branch.getMethodName()); + } + + @Test + void objectiveIdsAreStoredAndReflectedInToString() { + BytecodeInstruction instruction = branchInstruction(4); + Branch branch = new Branch(instruction, 9); + + assertNull(branch.getThenObjectiveId()); + assertNull(branch.getElseObjectiveId()); + + branch.setObjectiveIds("thenId", "elseId"); + + assertEquals("thenId", branch.getThenObjectiveId()); + assertEquals("elseId", branch.getElseObjectiveId()); + + String representation = branch.toString(); + assertTrue(representation.contains("I4")); + assertTrue(representation.contains("Branch 9")); + assertTrue(representation.contains("IFEQ")); + assertTrue(representation.contains("L104")); + assertTrue(representation.contains("[thenId, elseId]")); + } + + @Test + void equalsAndHashCodeDependOnInstructionAndId() { + Branch reference = new Branch(branchInstruction(5), 11); + Branch sameValues = new Branch(branchInstruction(5), 11); + Branch differentId = new Branch(branchInstruction(5), 12); + Branch differentInstruction = new Branch(branchInstruction(6), 11); + + assertEquals(reference, sameValues); + assertEquals(reference.hashCode(), sameValues.hashCode()); + + assertNotEquals(reference, differentId); + assertNotEquals(reference, differentInstruction); + assertNotEquals(reference, null); + assertNotEquals(reference, "branch"); + } + + private static BytecodeInstruction branchInstruction(int instructionId) { + BytecodeInstruction instruction = new BytecodeInstruction( + TEST_LOADER, + CLASS_NAME, + METHOD_NAME, + instructionId, + instructionId, + new JumpInsnNode(Opcodes.IFEQ, new LabelNode()) + ); + instruction.setLineNumber(100 + instructionId); + return instruction; + } + + private static BytecodeInstruction nonBranchInstruction(int instructionId) { + BytecodeInstruction instruction = new BytecodeInstruction( + TEST_LOADER, + CLASS_NAME, + METHOD_NAME, + instructionId, + instructionId, + new InsnNode(Opcodes.NOP) + ); + instruction.setLineNumber(10 + instructionId); + return instruction; + } +} + + diff --git a/core/src/main/kotlin/org/evomaster/core/EMConfig.kt b/core/src/main/kotlin/org/evomaster/core/EMConfig.kt index 03d11140e3..25baf51a03 100644 --- a/core/src/main/kotlin/org/evomaster/core/EMConfig.kt +++ b/core/src/main/kotlin/org/evomaster/core/EMConfig.kt @@ -1165,7 +1165,7 @@ class EMConfig { var avoidNonDeterministicLogs = false enum class Algorithm { - DEFAULT, SMARTS, MIO, RANDOM, WTS, MOSA, RW, + DEFAULT, SMARTS, MIO, RANDOM, WTS, MOSA, RW, DYNAMOSA, StandardGA, MonotonicGA, SteadyStateGA, BreederGA, CellularGA, OnePlusLambdaLambdaGA, MuLambdaEA, MuPlusLambdaEA, LIPS, CRO // GA variants still work-in-progress. } @@ -2374,6 +2374,13 @@ class EMConfig { " A negative value means all data in the collection will be asserted (i.e., no limit).") var maxAssertionForDataInCollection = 3 + /** + * Enable writing Control-Flow and related graphs (e.g., DOT/PNG) on the agent side. + * This controls only the persistence of graphs to disk; graph creation is controlled separately. + */ + @Cfg("Enable writing CFG/CDG graphs to disk on the agent side") + var writeCfg: Boolean = true + @Cfg("Specify whether to employ smart database clean to clear data in the database if the SUT has." + "`null` represents to employ the setting specified on the EM driver side") var employSmartDbClean: Boolean? = null diff --git a/core/src/main/kotlin/org/evomaster/core/Main.kt b/core/src/main/kotlin/org/evomaster/core/Main.kt index 39242b52d9..a0e66abbf1 100644 --- a/core/src/main/kotlin/org/evomaster/core/Main.kt +++ b/core/src/main/kotlin/org/evomaster/core/Main.kt @@ -639,6 +639,9 @@ class Main { EMConfig.Algorithm.MOSA -> Key.get(object : TypeLiteral>() {}) + + EMConfig.Algorithm.DYNAMOSA -> + Key.get(object : TypeLiteral>() {}) EMConfig.Algorithm.RW -> Key.get(object : TypeLiteral>() {}) @@ -684,6 +687,9 @@ class Main { EMConfig.Algorithm.MOSA -> Key.get(object : TypeLiteral>() {}) + EMConfig.Algorithm.DYNAMOSA -> + Key.get(object : TypeLiteral>() {}) + EMConfig.Algorithm.RW -> Key.get(object : TypeLiteral>() {}) EMConfig.Algorithm.LIPS -> @@ -724,6 +730,9 @@ class Main { EMConfig.Algorithm.MOSA -> Key.get(object : TypeLiteral>() {}) + EMConfig.Algorithm.DYNAMOSA -> + Key.get(object : TypeLiteral>() {}) + EMConfig.Algorithm.RW -> Key.get(object : TypeLiteral>() {}) EMConfig.Algorithm.LIPS -> @@ -764,6 +773,9 @@ class Main { EMConfig.Algorithm.MOSA -> Key.get(object : TypeLiteral>() {}) + EMConfig.Algorithm.DYNAMOSA -> + Key.get(object : TypeLiteral>() {}) + EMConfig.Algorithm.StandardGA -> Key.get(object : TypeLiteral>() {}) diff --git a/core/src/main/kotlin/org/evomaster/core/remote/service/RemoteController.kt b/core/src/main/kotlin/org/evomaster/core/remote/service/RemoteController.kt index 946bcaaf90..ee428e2bf7 100644 --- a/core/src/main/kotlin/org/evomaster/core/remote/service/RemoteController.kt +++ b/core/src/main/kotlin/org/evomaster/core/remote/service/RemoteController.kt @@ -3,6 +3,7 @@ package org.evomaster.core.remote.service import org.evomaster.client.java.controller.api.dto.* import org.evomaster.client.java.controller.api.dto.problem.param.DeriveParamResponseDto import org.evomaster.client.java.controller.api.dto.problem.param.DerivedParamChangeReqDto +import org.evomaster.client.java.instrumentation.shared.dto.ControlDependenceGraphDto import org.evomaster.core.problem.enterprise.param.DerivedParamChangeReq import org.evomaster.core.scheduletask.ScheduleTaskExecutor import org.evomaster.core.sql.DatabaseExecutor @@ -61,4 +62,6 @@ interface RemoteController : DatabaseExecutor, ScheduleTaskExecutor { fun close() fun deriveParams(deriveParams: List) : List + + fun getDynamosaControlDependenceGraphs(): List = emptyList() } \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/remote/service/RemoteControllerImplementation.kt b/core/src/main/kotlin/org/evomaster/core/remote/service/RemoteControllerImplementation.kt index 4d81a70f22..6e3b531539 100644 --- a/core/src/main/kotlin/org/evomaster/core/remote/service/RemoteControllerImplementation.kt +++ b/core/src/main/kotlin/org/evomaster/core/remote/service/RemoteControllerImplementation.kt @@ -7,6 +7,7 @@ import org.evomaster.client.java.controller.api.dto.* import org.evomaster.client.java.controller.api.dto.database.operations.* import org.evomaster.client.java.controller.api.dto.problem.param.DeriveParamResponseDto import org.evomaster.client.java.controller.api.dto.problem.param.DerivedParamChangeReqDto +import org.evomaster.client.java.instrumentation.shared.dto.ControlDependenceGraphDto import org.evomaster.client.java.controller.api.dto.problem.rpc.ScheduleTaskInvocationDto import org.evomaster.client.java.controller.api.dto.problem.rpc.ScheduleTaskInvocationsDto import org.evomaster.client.java.controller.api.dto.problem.rpc.ScheduleTaskInvocationsResult @@ -47,6 +48,7 @@ class RemoteControllerImplementation() : RemoteController{ private var extractSqlExecutionInfo = true private var cachedSutInfoDto : SutInfoDto? = null + private val pendingDynamosaCdgs: MutableList = mutableListOf() @Inject private lateinit var config: EMConfig @@ -258,6 +260,10 @@ class RemoteControllerImplementation() : RemoteController{ config.methodReplacementCategories() ) requestDto.advancedHeuristics = config.heuristicsForSQLAdvanced + + // Pass Dynamosa settings from core to controller/driver + requestDto.enableDynamosaGraphs = config.algorithm.toString() == "DYNAMOSA" + requestDto.writeCfg = config.writeCfg val response = try { makeHttpCall { @@ -365,7 +371,15 @@ class RemoteControllerImplementation() : RemoteController{ return null } - return getData(dto) + val result = getData(dto) + + if (result != null && result.dynamosaCdgs.isNotEmpty()) { + synchronized(pendingDynamosaCdgs) { + pendingDynamosaCdgs.addAll(result.dynamosaCdgs) + } + } + + return result } override fun deriveParams(deriveParams: List) : List{ @@ -385,6 +399,17 @@ class RemoteControllerImplementation() : RemoteController{ return dto?.data ?: listOf() } + override fun getDynamosaControlDependenceGraphs(): List { + synchronized(pendingDynamosaCdgs) { + if (pendingDynamosaCdgs.isEmpty()) { + return emptyList() + } + val copy = pendingDynamosaCdgs.toList() + pendingDynamosaCdgs.clear() + return copy + } + } + /** * execute [actionDto] through [ControllerConstants.NEW_ACTION] endpoints of EMController, diff --git a/core/src/main/kotlin/org/evomaster/core/search/algorithms/BranchDependencyGraph.kt b/core/src/main/kotlin/org/evomaster/core/search/algorithms/BranchDependencyGraph.kt new file mode 100644 index 0000000000..548406d016 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/search/algorithms/BranchDependencyGraph.kt @@ -0,0 +1,108 @@ +package org.evomaster.core.search.algorithms + +import org.evomaster.client.java.instrumentation.shared.dto.ControlDependenceGraphDto +import org.evomaster.core.search.service.IdMapper +import java.util.LinkedHashMap +import java.util.LinkedHashSet +import org.slf4j.LoggerFactory + +/** + * Lightweight graph that captures parent→child dependencies between branch targets. + * Nodes are identified by the numeric ids assigned by the instrumentation agent + */ + +private val log = LoggerFactory.getLogger(BranchDependencyGraph::class.java) +class BranchDependencyGraph( + private val idMapper: IdMapper +) { + + private val children: MutableMap> = LinkedHashMap() + private val roots: MutableSet = LinkedHashSet() + private val objectives: MutableSet = LinkedHashSet() + private val parentCounts: MutableMap = LinkedHashMap() + + fun addGraphs(cdgs: List) { + for (dto in cdgs) { + // method objectives are the objectives that are specific to the method + val methodObjectives = registerObjectives(dto) + // register edges between the objectives + registerEdges(dto) + // register roots are the method objectives that are the roots of the graph + registerRoots(dto, methodObjectives) + } + } + + private fun registerObjectives(dto: ControlDependenceGraphDto): Set { + val methodObjectives = LinkedHashSet() + + for (obj in dto.objectives) { + val id = obj.id + + // register the objective + objectives.add(id) + // method objectives are the objectives that are specific to the method + methodObjectives.add(id) + + + // register the parent counts. + // if the objective is not in the parent counts, add it with a count of 0 + // this is used to determine the roots of the graph + // if the parent counts is zero, then the objective is a root + if (!parentCounts.containsKey(id)) { + parentCounts[id] = 0 + } + + // add the descriptive id to the id mapper + if (obj.descriptiveId != null) { + idMapper.addMapping(id, obj.descriptiveId) + } + } + + return methodObjectives + } + + private fun registerEdges(dto: ControlDependenceGraphDto) { + for (edge in dto.edges) { + // register the parent child relationship + registerParentChild(edge.parentObjectiveId, edge.childObjectiveId) + } + } + + private fun registerParentChild(parentId: Int, childId: Int) { + // register the parent child relationship + // If the parent is not in the children map (meaning it is a new parent), add it with an empty set + val set = children.getOrPut(parentId) { LinkedHashSet() } + // add the child to the parent's set of children + set.add(childId) + // increment the parent count for the child + val newCount = (parentCounts[childId] ?: 0) + 1 + // update the parent count for the child + parentCounts[childId] = newCount + // if the parent count is greater than 0, then the child is not a root, so remove it from the roots set + if (newCount > 0) { + roots.remove(childId) + } + } + + private fun registerRoots(dto: ControlDependenceGraphDto, methodObjectives: Set) { + + // potential roots are the root objectives that are specific to the method + val potentialRoots = dto.rootObjectiveIds + + // register the roots, just if they are roots according to the parent counts + for (root in potentialRoots) { + if (isRoot(root)) { + roots.add(root) + } + } + } + + private fun isRoot(id: Int): Boolean = (parentCounts[id] ?: 0) == 0 + + fun getRoots(): Set = LinkedHashSet(roots) + + fun getChildren(parent: Int): Set = children[parent]?.toSet() ?: emptySet() + + fun getAllObjectives(): Set = LinkedHashSet(objectives) +} + diff --git a/core/src/main/kotlin/org/evomaster/core/search/algorithms/DynamosaAlgorithm.kt b/core/src/main/kotlin/org/evomaster/core/search/algorithms/DynamosaAlgorithm.kt new file mode 100644 index 0000000000..7566a3670f --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/search/algorithms/DynamosaAlgorithm.kt @@ -0,0 +1,349 @@ +package org.evomaster.core.search.algorithms + +import org.evomaster.core.EMConfig +import org.evomaster.core.search.EvaluatedIndividual +import org.evomaster.core.search.Individual +import org.evomaster.core.search.service.SearchAlgorithm +import org.evomaster.core.search.service.IdMapper +import org.evomaster.core.logging.LoggingUtil +import java.util.ArrayList +import com.google.inject.Inject +import org.evomaster.core.remote.service.RemoteController + + + +/** + * Implementation of DYNAMOSA from: + * Automated Test Case Generation as a Many-Objective Optimisation Problem with Dynamic Selection of the Targets + * This algorithm is a variant of the MOSA algorithm that uses a dynamic, reduced focus set of targets per generation. + * The dynamic focus set of targets is computed using the branch dependency graph. + */ +class DynaMosaAlgorithm : SearchAlgorithm() where T : Individual { + + private class Data(val ind: EvaluatedIndividual<*>) { + + var rank = -1 + var crowdingDistance = -1 + } + + private var population: MutableList = mutableListOf() + + // Dynamic subset of objectives to optimize this generation + private var focusTargets: MutableSet = mutableSetOf() + private lateinit var goalsManager: MulticriteriaManager + @Inject + lateinit var idMapper: IdMapper + @Inject + lateinit var remoteController: RemoteController + + private val log = LoggingUtil.getInfoLogger() + + override fun getType(): EMConfig.Algorithm { + return EMConfig.Algorithm.DYNAMOSA + } + + override fun setupBeforeSearch() { + + population.clear() + + goalsManager = MulticriteriaManager(archive, idMapper) + + initPopulation() + + val newCdgs = remoteController.getDynamosaControlDependenceGraphs() + if (newCdgs.isNotEmpty()) { + goalsManager.addControlDependenceGraphs(newCdgs) + } + + sortPopulation() + } + + override fun searchOnce() { + + val newCdgs = remoteController.getDynamosaControlDependenceGraphs() + if (newCdgs.isNotEmpty()) { + goalsManager.addControlDependenceGraphs(newCdgs) + } + + val n = config.populationSize + + //new generation + + val nextPop: MutableList = mutableListOf() + + while (nextPop.size < n-1) { + + var ind = selection() + + getMutatator().mutateAndSave(ind, archive) + ?.let{nextPop.add(Data(it))} + + if (!time.shouldContinueSearch()) { + break + } + } + // generate one random solution + var ie = sampleIndividual() + nextPop.add(Data(ie as EvaluatedIndividual)) + + population.addAll(nextPop) + + val moreCdgs = remoteController.getDynamosaControlDependenceGraphs() + if (moreCdgs.isNotEmpty()) { + goalsManager.addControlDependenceGraphs(moreCdgs) + } + + sortPopulation() + } + + + private fun sortPopulation() { + + // Use manager-computed uncovered goals from complete CFGs + val notCovered = goalsManager.getUncoveredGoals() + + if(notCovered.isEmpty()){ + //Trivial problem: everything covered in first population + return + } + + // DynaMOSA: use MultiCriteriaManager to get dynamic goals (branch roots among uncovered) + goalsManager.refreshGoals() + val dynamic = goalsManager.getCurrentGoals() + focusTargets.clear() + focusTargets.addAll(dynamic) + + val fronts = preferenceSorting(focusTargets, population) + + var remain: Int = config.populationSize + var index = 0 + population.clear() + + // Obtain the next front + var front = fronts[index] + + while (front!=null && remain > 0 && remain >= front.size && front.isNotEmpty()) { + // Assign crowding distance to individuals + subvectorDominance(focusTargets, front) + // Add the individuals of this front + for (d in front) { + population.add(d) + } + + // Decrement remain + remain = remain - front.size + + // Obtain the next front + index += 1 + if (remain > 0) { + front = fronts[index] + } // if + } // while + + // Remain is less than front(index).size, insert only the best one + if (remain > 0 && front!=null && front.isNotEmpty()) { + subvectorDominance(focusTargets, front) + var front2 = front.sortedWith(compareBy { - it.crowdingDistance }) + .toMutableList() + for (k in 0..remain - 1) { + population.add(front2[k]) + } // for + + } // if + + } + + private fun subvectorDominance(notCovered: Set, list: List){ + /* + see: + Substitute Distance Assignments in NSGA-II for + Handling Many-Objective Optimization Problems + */ + + list.forEach { i -> + i.crowdingDistance = 0 + list.filter { j -> j!=i }.forEach { j -> + val v = svd(notCovered, i, j) + if(v > i.crowdingDistance){ + i.crowdingDistance = v + } + } + } + } + + + private fun svd(notCovered: Set, i: Data, j: Data) : Int{ + var cnt = 0 + for(t in notCovered){ + if(i.ind.fitness.getHeuristic(t) > j.ind.fitness.getHeuristic(t)){ + cnt++ + } + } + return cnt + } + + + /* + See: Preference sorting as discussed in the TSE paper for DynaMOSA + */ + private fun preferenceSorting(notCovered: Set, list: List): HashMap> { + + val fronts = HashMap>() + + // compute the first front using the Preference Criteria + val frontZero = mosaPreferenceCriterion(notCovered, list) + fronts.put(0, ArrayList(frontZero)) + LoggingUtil.getInfoLogger().apply { + debug("First front size : ${frontZero.size}") + } + + // compute the remaining non-dominated Fronts + val remaining_solutions: MutableList = mutableListOf() + remaining_solutions.addAll(list) + remaining_solutions.removeAll(frontZero) + + var selected_solutions = frontZero.size + var front_index = 1 + + while (selected_solutions < config.populationSize && remaining_solutions.isNotEmpty()){ + var front: MutableList = getNonDominatedFront(notCovered, remaining_solutions) + fronts.put(front_index, front) + for (sol in front){ + sol.rank = front_index + } + remaining_solutions.removeAll(front) + + selected_solutions += front.size + + front_index += 1 + + LoggingUtil.getInfoLogger().apply { + debug("Selected Solutions : ${selected_solutions}") + } + } + return fronts + } + + /** + * It retrieves the front of non-dominated solutions from a list + */ + private fun getNonDominatedFront(notCovered: Set, remaining_sols: List): MutableList{ + var front: MutableList = mutableListOf() + var isDominated: Boolean + + for (p in remaining_sols) { + isDominated = false + val dominatedSolutions = ArrayList(remaining_sols.size) + for (best in front) { + val flag = compare(p, best, notCovered) + if (flag == -1) { + dominatedSolutions.add(best) + } + if (flag == +1) { + isDominated = true + } + } + + if (isDominated) + continue + + front.removeAll(dominatedSolutions) + front.add(p) + + } + return front + } + + /** + * Fast routine based on the Dominance Comparator discussed in + * "Automated Test Case Generation as a Many-Objective Optimisation Problem with Dynamic + * Selection of the Targets" + */ + private fun compare(x: Data, y: Data, notCovered: Set): Int { + var dominatesX = false + var dominatesY = false + + for (index in 1..notCovered.size) { + if (x.ind.fitness.getHeuristic(index) > y.ind.fitness.getHeuristic(index)) + dominatesX = true + if (y.ind.fitness.getHeuristic(index) > x.ind.fitness.getHeuristic(index)) + dominatesY = true + + // if the both do not dominates each other, we don't + // need to iterate over all the other targets + if (dominatesX && dominatesY) + return 0 + } + + if (dominatesX == dominatesY) + return 0 + + else if (dominatesX) + return -1 + + else // (dominatesY) + return +1 + } + + private fun mosaPreferenceCriterion(notCovered: Set, list: List): HashSet { + var frontZero: HashSet = HashSet() + + notCovered.forEach { t -> + var chosen = list[0] + list.forEach { data -> + if (data.ind.fitness.getHeuristic(t) > chosen.ind.fitness.getHeuristic(t)) { + // recall: maximization problem + chosen = data + } else if (data.ind.fitness.getHeuristic(t) == chosen.ind.fitness.getHeuristic(t) + && data.ind.individual.size() < chosen.ind.individual.size()){ + // Secondary criterion based on tests lengths + chosen = data + } + } + // MOSA preference criterion: the best for a target gets Rank 0 + chosen.rank = 0 + frontZero.add(chosen) + } + return frontZero + } + + private fun selection(): EvaluatedIndividual { + + // the population is not fully sorted + var min = randomness.nextInt(population.size) + + (0 until config.tournamentSize-1).forEach { + val sel = randomness.nextInt(population.size) + if (population[sel].rank < population[min].rank) { + min = sel + } else if (population[sel].rank == population[min].rank){ + if (population[sel].crowdingDistance < population[min].crowdingDistance) + min = sel + } + } + + return (population[min].ind as EvaluatedIndividual).copy() + } + + + private fun initPopulation() { + + val n = config.populationSize + + for (i in 1..n) { + sampleIndividual()?.run { population.add(Data(this)) } + + if (!time.shouldContinueSearch()) { + break + } + } + } + + private fun sampleIndividual(): EvaluatedIndividual? { + + return ff.calculateCoverage(sampler.sample(), modifiedSpec = null) + ?.also { archive.addIfNeeded(it) } + } +} + + diff --git a/core/src/main/kotlin/org/evomaster/core/search/algorithms/MulticriteriaManager.kt b/core/src/main/kotlin/org/evomaster/core/search/algorithms/MulticriteriaManager.kt new file mode 100644 index 0000000000..7f80716d37 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/search/algorithms/MulticriteriaManager.kt @@ -0,0 +1,122 @@ +package org.evomaster.core.search.algorithms + +import org.evomaster.client.java.instrumentation.shared.ObjectiveNaming +import org.evomaster.client.java.instrumentation.shared.dto.ControlDependenceGraphDto +import org.evomaster.core.search.service.Archive +import org.evomaster.core.search.service.IdMapper +import org.evomaster.core.logging.LoggingUtil +import java.util.LinkedHashSet + +/** + * MultiCriteriaManager for DynaMOSA + */ +class MulticriteriaManager( + private val archive: Archive<*>, + private val idMapper: IdMapper, +) { + + private val log = LoggingUtil.getInfoLogger() + + private val branchGraph: BranchDependencyGraph = BranchDependencyGraph(idMapper) + + /** + * Current generation focus set of targets (numeric ids from ObjectiveRecorder mapping). + */ + private val currentGoals: LinkedHashSet = LinkedHashSet() + + fun addControlDependenceGraphs(cdgs: List) { + + if (cdgs.isEmpty()) { + return + } + branchGraph.addGraphs(cdgs) + + currentGoals.addAll(branchGraph.getRoots()) + } + + /** + * Refresh the current goals using uncovered targets and the dependency graph(s). + * Seeds with roots ∩ uncovered, then adds uncovered children of covered parents. + */ + fun refreshGoals() { + val uncovered: Set = getUncoveredGoals() + val covered: Set = getCoveredGoals() + currentGoals.clear() + + val branchRoots = branchGraph.getRoots() + val seeded = branchRoots.intersect(uncovered) + val expanded: MutableSet = LinkedHashSet() + + for (p in covered) { + for (c in branchGraph.getChildren(p)) { + if (c in uncovered) { + expanded.add(c) + } + } + } + + val next = LinkedHashSet() + next.addAll(seeded) + next.addAll(expanded) + if (next.isNotEmpty()) { + currentGoals.addAll(next) + } else { + currentGoals.addAll(uncovered) + } + } + + /** + * Snapshot of current goals. + */ + fun getCurrentGoals(): Set = LinkedHashSet(currentGoals) + + /** + * Roots of the branch dependency graph (base roots only). + */ + fun getBranchRoots(): Set = branchGraph.getRoots() + + /** + * (all CFG-derived branch ids) minus (archive-covered branch ids) + */ + fun getUncoveredGoals(): Set { + val allBranchObjectives = branchGraph.getAllObjectives() + if (allBranchObjectives.isEmpty()) return emptySet() + val covered: Set = getCoveredGoals() + return allBranchObjectives.minus(covered) + } + + /** + * Use archive as source of covered targets, then keep only Branch targets + */ + fun getCoveredGoals(): Set { + // Get Covered Targets from the Archive + val covered = archive.coveredTargets() + if (covered.isEmpty()) { + return emptySet() + } + + val result = LinkedHashSet() + val allObjectives = branchGraph.getAllObjectives() + + for (id in covered) { + // Consider only the objectives that are part of the branch dependency graph + + if (allObjectives.contains(id)) { + log.info("Covered target $id is in the branch dependency graph") + val desc = idMapper.getDescriptiveId(id) + log.info("Descriptive ID: $desc") + result.add(id) + } else { + // Sanity check: if it's a branch according to its descriptive id but isn't in the branch dependency graph, warn + val desc = idMapper.getDescriptiveId(id) + if (desc != null && desc.startsWith(ObjectiveNaming.BRANCH)) { + log.warn("Covered target $id ($desc) looks like a Branch but is not in BranchDependencyGraph!") + } + } + } + return result + } + +} + + diff --git a/core/src/test/kotlin/org/evomaster/core/search/algorithms/BranchDependencyGraphTest.kt b/core/src/test/kotlin/org/evomaster/core/search/algorithms/BranchDependencyGraphTest.kt new file mode 100644 index 0000000000..fed09ed2bf --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/search/algorithms/BranchDependencyGraphTest.kt @@ -0,0 +1,111 @@ +package org.evomaster.core.search.algorithms + +import io.mockk.mockk +import io.mockk.verify +import org.evomaster.client.java.instrumentation.shared.dto.ControlDependenceGraphDto +import org.evomaster.core.search.service.IdMapper +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class BranchDependencyGraphTest { + + private lateinit var idMapper: IdMapper + private lateinit var graph: BranchDependencyGraph + + @BeforeEach + fun setUp() { + idMapper = mockk(relaxed = true) + graph = BranchDependencyGraph(idMapper) + } + + @Test + fun test_addGraphsRegistersObjectivesAndRoots() { + val dto = createGraph( + objectives = listOf(1, 2), + rootIds = listOf(1), + edges = listOf(1 to 2) + ) + + graph.addGraphs(listOf(dto)) + + assertEquals(setOf(1, 2), graph.getAllObjectives()) + assertEquals(setOf(1), graph.getRoots()) + verify { idMapper.addMapping(1, "desc_1") } + verify { idMapper.addMapping(2, "desc_2") } + } + + @Test + fun test_getChildrenReturnsRegisteredEdges() { + val dto = createGraph( + objectives = listOf(10, 11, 12), + rootIds = listOf(10), + edges = listOf(10 to 11, 10 to 12) + ) + + graph.addGraphs(listOf(dto)) + + assertEquals(setOf(11, 12), graph.getChildren(10)) + assertTrue(graph.getChildren(999).isEmpty()) + } + + @Test + fun test_getAllObjectivesAggregatesAcrossGraphs() { + val first = createGraph( + objectives = listOf(1, 2), + rootIds = listOf(1), + edges = listOf(1 to 2) + ) + val second = createGraph( + objectives = listOf(20, 21), + rootIds = listOf(20), + edges = listOf(20 to 21) + ) + + graph.addGraphs(listOf(first, second)) + + assertEquals(setOf(1, 2, 20, 21), graph.getAllObjectives()) + assertEquals(setOf(1, 20), graph.getRoots()) + } + + @Test + fun test_childIsNotRootIfItHasParent() { + val dto = createGraph( + objectives = listOf(1, 2), + rootIds = listOf(1, 2), + edges = listOf(1 to 2) + ) + + graph.addGraphs(listOf(dto)) + + assertEquals(setOf(1), graph.getRoots()) + } + + private fun createGraph( + objectives: List, + rootIds: List, + edges: List> + ): ControlDependenceGraphDto { + val dto = ControlDependenceGraphDto() + dto.className = "Clazz" + dto.methodName = "method" + + dto.objectives = objectives.map { id -> + ControlDependenceGraphDto.BranchObjectiveDto().apply { + this.id = id + this.descriptiveId = "desc_$id" + } + } + dto.rootObjectiveIds = rootIds + dto.edges = edges.map { (parent, child) -> + ControlDependenceGraphDto.DependencyEdgeDto().apply { + parentObjectiveId = parent + childObjectiveId = child + } + } + return dto + } +} + + diff --git a/core/src/test/kotlin/org/evomaster/core/search/algorithms/DynaMosaAlgorithmTest.kt b/core/src/test/kotlin/org/evomaster/core/search/algorithms/DynaMosaAlgorithmTest.kt new file mode 100644 index 0000000000..7accaab56b --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/search/algorithms/DynaMosaAlgorithmTest.kt @@ -0,0 +1,99 @@ +package org.evomaster.core.search.algorithms + +import com.google.inject.AbstractModule +import com.google.inject.Injector +import com.google.inject.Key +import com.google.inject.Module +import com.google.inject.TypeLiteral +import com.netflix.governator.guice.LifecycleInjector +import org.evomaster.core.BaseModule +import org.evomaster.core.EMConfig +import org.evomaster.core.search.algorithms.onemax.OneMaxIndividual +import org.evomaster.core.search.algorithms.onemax.OneMaxModule +import org.evomaster.core.search.algorithms.onemax.OneMaxSampler +import org.evomaster.core.search.service.ExecutionPhaseController +import org.evomaster.core.remote.service.RemoteController +import org.evomaster.client.java.controller.api.dto.* +import org.evomaster.client.java.controller.api.dto.problem.param.DeriveParamResponseDto +import org.evomaster.client.java.controller.api.dto.problem.param.DerivedParamChangeReqDto +import org.evomaster.client.java.controller.api.dto.problem.rpc.ScheduleTaskInvocationsDto +import org.evomaster.client.java.controller.api.dto.problem.rpc.ScheduleTaskInvocationsResult +import org.evomaster.client.java.controller.api.dto.database.operations.* +import org.evomaster.client.java.controller.api.dto.ActionDto +import org.evomaster.client.java.controller.api.dto.ActionResponseDto +import org.evomaster.client.java.instrumentation.shared.dto.ControlDependenceGraphDto +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class DynaMosaAlgorithmTest { + + private lateinit var injector: Injector + + @BeforeEach + fun setUp() { + injector = LifecycleInjector.builder() + .withModules(* arrayOf(OneMaxModule(), BaseModule(), DynaMosaTestModule())) + .build().createInjector() + } + + @Test + fun testDynamosaFindsOptimumOnOneMax() { + val dynamosa = injector.getInstance( + Key.get(object : TypeLiteral>() {}) + ) + + val config = injector.getInstance(EMConfig::class.java) + config.maxEvaluations = 10_000 + config.stoppingCriterion = EMConfig.StoppingCriterion.ACTION_EVALUATIONS + + val epc = injector.getInstance(ExecutionPhaseController::class.java) + epc.startSearch() + val solution = dynamosa.search() + epc.finishSearch() + + assertEquals(1, solution.individuals.size) + assertEquals( + OneMaxSampler.DEFAULT_N.toDouble(), + solution.overall.computeFitnessScore(), + 0.001 + ) + } +} + +private class DynaMosaTestModule : AbstractModule() { + override fun configure() { + bind(RemoteController::class.java).toInstance(DummyRemoteController()) + } +} + +private class DummyRemoteController : RemoteController { + override fun getSutInfo(): SutInfoDto? = null + override fun getControllerInfo(): ControllerInfoDto? = null + override fun startSUT(): Boolean = true + override fun stopSUT(): Boolean = true + override fun resetSUT(): Boolean = true + override fun checkConnection() {} + override fun startANewSearch(): Boolean = true + override fun getTestResults( + ids: Set, + ignoreKillSwitch: Boolean, + fullyCovered: Boolean, + descriptiveIds: Boolean + ): TestResultsDto? = null + + override fun executeNewRPCActionAndGetResponse(actionDto: ActionDto): ActionResponseDto? = null + override fun postSearchAction(postSearchActionDto: PostSearchActionDto): Boolean = true + override fun registerNewAction(actionDto: ActionDto): Boolean = true + override fun address(): String = "dummy" + override fun close() {} + override fun deriveParams(deriveParams: List): List = emptyList() + override fun getDynamosaControlDependenceGraphs(): List = emptyList() + + override fun executeDatabaseCommand(dto: DatabaseCommandDto): Boolean = true + override fun executeDatabaseCommandAndGetQueryResults(dto: DatabaseCommandDto): QueryResultDto? = null + override fun executeDatabaseInsertionsAndGetIdMapping(dto: DatabaseCommandDto): InsertionResultsDto? = null + override fun executeMongoDatabaseInsertions(dto: MongoDatabaseCommandDto): MongoInsertionResultsDto? = null + override fun invokeScheduleTasksAndGetResults(dtos: ScheduleTaskInvocationsDto): ScheduleTaskInvocationsResult? = null +} + diff --git a/core/src/test/kotlin/org/evomaster/core/search/algorithms/MulticriteriaManagerTest.kt b/core/src/test/kotlin/org/evomaster/core/search/algorithms/MulticriteriaManagerTest.kt new file mode 100644 index 0000000000..3f39ce03f3 --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/search/algorithms/MulticriteriaManagerTest.kt @@ -0,0 +1,130 @@ +package org.evomaster.core.search.algorithms + +import io.mockk.every +import io.mockk.mockk +import org.evomaster.client.java.instrumentation.shared.ObjectiveNaming +import org.evomaster.client.java.instrumentation.shared.dto.ControlDependenceGraphDto +import org.evomaster.core.search.service.Archive +import org.evomaster.core.search.service.IdMapper +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class MulticriteriaManagerTest { + + private lateinit var archive: Archive<*> + private lateinit var idMapper: IdMapper + private lateinit var manager: MulticriteriaManager + + @BeforeEach + fun setUp() { + archive = mockk(relaxed = true) + idMapper = mockk(relaxed = true) + } + + @Test + fun test_initializationWithGraphs() { + val dto = createSimpleGraphDto(rootId = 1, childId = 2) + val graphs = listOf(dto) + + manager = MulticriteriaManager(archive, idMapper) + manager.addControlDependenceGraphs(graphs) + + val roots = manager.getBranchRoots() + assertTrue(roots.contains(1)) + assertEquals(1, roots.size) + + // Initially, current goals should be the roots + val currentGoals = manager.getCurrentGoals() + assertEquals(setOf(1), currentGoals) + } + + @Test + fun test_refreshGoals_expandsChildrenWhenParentIsCovered() { + + // Graph: 1 -> 2 + val dto = createSimpleGraphDto(rootId = 1, childId = 2) + manager = MulticriteriaManager(archive, idMapper) + manager.addControlDependenceGraphs(listOf(dto)) + + // Mock archive to say 1 is covered + every { archive.coveredTargets() } returns setOf(1) + + // Mock idMapper to return descriptive IDs if needed (though getCoveredGoals logic uses it mainly for logging/sanity) + every { idMapper.getDescriptiveId(1) } returns ObjectiveNaming.BRANCH + "_1" + + manager.refreshGoals() + val goals = manager.getCurrentGoals() + + // Assert + // Since 1 is covered, it should expand to children of 1 (which is 2). + assertEquals(setOf(2), goals) + } + + @Test + fun test_getCoveredGoals_filtersOutTargetsNotInGraph() { + // Arrange + // Graph has only ID 1. + val dto = createSingleNodeGraphDto(id = 1) + manager = MulticriteriaManager(archive, idMapper) + manager.addControlDependenceGraphs(listOf(dto)) + + // Archive reports 1 and 99 (ghost ID) as covered + every { archive.coveredTargets() } returns setOf(1, 99) + every { idMapper.getDescriptiveId(1) } returns ObjectiveNaming.BRANCH + "_1" + every { idMapper.getDescriptiveId(99) } returns ObjectiveNaming.BRANCH + "_99" + + // Act + val covered = manager.getCoveredGoals() + + // Assert + // Should only contain 1, because 99 is not in the BranchDependencyGraph + assertTrue(covered.contains(1)) + assertFalse(covered.contains(99)) + assertEquals(1, covered.size) + } + + // --- Helpers --- + + private fun createSimpleGraphDto(rootId: Int, childId: Int): ControlDependenceGraphDto { + val dto = ControlDependenceGraphDto() + dto.className = "TestClass" + dto.methodName = "testMethod" + + val rootObj = ControlDependenceGraphDto.BranchObjectiveDto() + rootObj.id = rootId + rootObj.descriptiveId = ObjectiveNaming.BRANCH + "_$rootId" + + val childObj = ControlDependenceGraphDto.BranchObjectiveDto() + childObj.id = childId + childObj.descriptiveId = ObjectiveNaming.BRANCH + "_$childId" + + dto.objectives = listOf(rootObj, childObj) + dto.rootObjectiveIds = listOf(rootId) + + val edge = ControlDependenceGraphDto.DependencyEdgeDto() + edge.parentObjectiveId = rootId + edge.childObjectiveId = childId + + dto.edges = listOf(edge) + + return dto + } + + private fun createSingleNodeGraphDto(id: Int): ControlDependenceGraphDto { + val dto = ControlDependenceGraphDto() + dto.className = "TestClass" + dto.methodName = "testMethod" + + val obj = ControlDependenceGraphDto.BranchObjectiveDto() + obj.id = id + obj.descriptiveId = ObjectiveNaming.BRANCH + "_$id" + + dto.objectives = listOf(obj) + dto.rootObjectiveIds = listOf(id) + dto.edges = emptyList() + + return dto + } +} + diff --git a/docs/options.md b/docs/options.md index 9980693f46..e5c545abde 100644 --- a/docs/options.md +++ b/docs/options.md @@ -68,7 +68,7 @@ There are 3 types of options: |`addPreDefinedTests`| __Boolean__. Add predefined tests at the end of the search. An example is a test to fetch the schema of RESTful APIs. *Default value*: `true`.| |`addTestComments`| __Boolean__. Add summary comments on each test. *Default value*: `true`.| |`advancedBlackBoxCoverage`| __Boolean__. Apply more advanced coverage criteria for black-box testing. This can result in larger generated test suites. *Default value*: `true`.| -|`algorithm`| __Enum__. The algorithm used to generate test cases. The default depends on whether black-box or white-box testing is done. *Valid values*: `DEFAULT, SMARTS, MIO, RANDOM, WTS, MOSA, RW, StandardGA, MonotonicGA, SteadyStateGA, BreederGA, CellularGA, OnePlusLambdaLambdaGA, MuLambdaEA, MuPlusLambdaEA, LIPS, CRO`. *Default value*: `DEFAULT`.| +|`algorithm`| __Enum__. The algorithm used to generate test cases. The default depends on whether black-box or white-box testing is done. *Valid values*: `DEFAULT, SMARTS, MIO, RANDOM, WTS, MOSA, RW, DYNAMOSA, StandardGA, MonotonicGA, SteadyStateGA, BreederGA, CellularGA, OnePlusLambdaLambdaGA, MuLambdaEA, MuPlusLambdaEA, LIPS, CRO`. *Default value*: `DEFAULT`.| |`allowInvalidData`| __Boolean__. When generating data, allow in some cases to use invalid values on purpose. *Default value*: `true`.| |`appendToStatisticsFile`| __Boolean__. Whether should add to an existing statistics file, instead of replacing it. *Default value*: `false`.| |`archiveAfterMutationFile`| __String__. Specify a path to save archive after each mutation during search, only useful for debugging. *DEBUG option*. *Default value*: `archive.csv`.| @@ -233,6 +233,7 @@ There are 3 types of options: |`useResponseDataPool`| __Boolean__. Enable the collection of response data, to feed new individuals based on field names matching. *Default value*: `true`.| |`useTimeInFeedbackSampling`| __Boolean__. Whether to use timestamp info on the execution time of the tests for sampling (e.g., to reward the quickest ones). *Default value*: `true`.| |`weightBasedMutationRate`| __Boolean__. Whether to enable a weight-based mutation rate. *Default value*: `true`.| +|`writeCfg`| __Boolean__. Enable writing CFG/CDG graphs to disk on the agent side. *Default value*: `true`.| |`writeExtraHeuristicsFile`| __Boolean__. Whether we should collect data on the extra heuristics. Only needed for experiments. *Default value*: `false`.| |`writeStatistics`| __Boolean__. Whether or not writing statistics of the search process. This is only needed when running experiments with different parameter settings. *Default value*: `false`.| |`writeWFCReport`| __Boolean__. Output a JSON file representing statistics of the fuzzing session, written in the WFC Report format. This also includes a index.html web application to visualize such data. *Default value*: `true`.|