diff --git a/pom.xml b/pom.xml
index 00edd45..d9be858 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,6 +7,7 @@
0.0.4-SNAPSHOT
+
ch.qos.logback
logback-classic
@@ -22,8 +23,7 @@
test
-
-
+
org.mockito
mockito-core
@@ -31,7 +31,7 @@
test
-
+
org.junit.jupiter
junit-jupiter-api
@@ -39,6 +39,15 @@
test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.10.0
+ test
+
+
+
uk.org.webcompere
system-stubs-jupiter
@@ -128,15 +137,6 @@
2.20.0
-
- org.junit.jupiter
- junit-jupiter-engine
- 5.9.2
- test
-
-
-
-
17
diff --git a/src/main/java/analysis/GoblintAnalysis.java b/src/main/java/analysis/GoblintAnalysis.java
index 74fe80d..fc27700 100644
--- a/src/main/java/analysis/GoblintAnalysis.java
+++ b/src/main/java/analysis/GoblintAnalysis.java
@@ -121,6 +121,9 @@ public void analyze(Collection extends Module> files, AnalysisConsumer consume
log.info("---------------------- Analysis started ----------------------");
lastAnalysisTask = reanalyse().thenAccept(response -> {
+ for (AnalysisResult analysisResult : response) {
+ System.out.println(analysisResult.toString());
+ }
consumer.consume(new ArrayList<>(response), source());
log.info("--------------------- Analysis finished ----------------------");
@@ -152,7 +155,6 @@ public void analyze(Collection extends Module> files, AnalysisConsumer consume
public CompletableFuture> reanalyse() {
//return goblintService.analyze(new AnalyzeParams(true))
return goblintService.analyze(new AnalyzeParams(!gobpieConfiguration.useIncrementalAnalysis()))
-
.thenCompose(this::getComposedAnalysisResults);
}
diff --git a/src/main/java/analysis/GoblintCFGAnalysisResult.java b/src/main/java/analysis/GoblintCFGAnalysisResult.java
index d009896..e72d65a 100644
--- a/src/main/java/analysis/GoblintCFGAnalysisResult.java
+++ b/src/main/java/analysis/GoblintCFGAnalysisResult.java
@@ -9,6 +9,7 @@
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Objects;
/**
* The Class GoblintCFGAnalysisResult.
@@ -72,4 +73,28 @@ public Pair repair() {
public String code() {
return null;
}
+
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ GoblintCFGAnalysisResult that = (GoblintCFGAnalysisResult) o;
+ return Objects.equals(pos, that.pos) && Objects.equals(title, that.title) && Objects.equals(funName, that.funName) && Objects.equals(related, that.related);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(pos, title, funName, related);
+ }
+
+ @Override
+ public String toString() {
+ return "GoblintCFGAnalysisResult{" +
+ "pos=" + pos +
+ ", title='" + title + '\'' +
+ ", funName='" + funName + '\'' +
+ ", related=" + related +
+ '}';
+ }
}
diff --git a/src/main/java/analysis/GoblintMessagesAnalysisResult.java b/src/main/java/analysis/GoblintMessagesAnalysisResult.java
index f74496c..1c30331 100644
--- a/src/main/java/analysis/GoblintMessagesAnalysisResult.java
+++ b/src/main/java/analysis/GoblintMessagesAnalysisResult.java
@@ -8,6 +8,7 @@
import org.eclipse.lsp4j.DiagnosticSeverity;
import java.util.ArrayList;
+import java.util.Objects;
/**
* The Class GoblintMessagesAnalysisResult.
@@ -108,4 +109,27 @@ public String code() {
return null;
}
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ GoblintMessagesAnalysisResult that = (GoblintMessagesAnalysisResult) o;
+ return Objects.equals(group_text, that.group_text) && Objects.equals(text, that.text) && Objects.equals(pos, that.pos) && Objects.equals(severity, that.severity) && Objects.equals(related, that.related);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(group_text, text, pos, severity, related);
+ }
+
+ @Override
+ public String toString() {
+ return "GoblintMessagesAnalysisResult{" +
+ "group_text='" + group_text + '\'' +
+ ", text='" + text + '\'' +
+ ", pos=" + pos.toString() +
+ ", severity='" + severity + '\'' +
+ ", related=" + related +
+ '}';
+ }
}
diff --git a/src/main/java/api/messages/GoblintMessagesResult.java b/src/main/java/api/messages/GoblintMessagesResult.java
index f021b3f..0059fa6 100644
--- a/src/main/java/api/messages/GoblintMessagesResult.java
+++ b/src/main/java/api/messages/GoblintMessagesResult.java
@@ -92,7 +92,7 @@ public static class Group implements MultiPiece {
* @return A collection of AnalysisResult objects.
*/
public List convert(List tags, String severity, boolean explode) {
- return explode
+ return explode && this.group_loc != null
? convertGroupExplode(tags, severity)
: convertGroup(tags, severity);
}
diff --git a/src/main/java/api/messages/GoblintPosition.java b/src/main/java/api/messages/GoblintPosition.java
index 64debad..6096f84 100644
--- a/src/main/java/api/messages/GoblintPosition.java
+++ b/src/main/java/api/messages/GoblintPosition.java
@@ -5,6 +5,7 @@
import java.io.Reader;
import java.net.URL;
+import java.util.Objects;
/**
* The Class GoblintPosition.
@@ -81,4 +82,28 @@ public Reader getReader() {
public URL getURL() {
return sourcefileURL;
}
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ GoblintPosition that = (GoblintPosition) o;
+ return columnStart == that.columnStart && columnEnd == that.columnEnd && lineStart == that.lineStart && lineEnd == that.lineEnd && Objects.equals(sourcefileURL, that.sourcefileURL);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(columnStart, columnEnd, lineStart, lineEnd, sourcefileURL);
+ }
+
+ @Override
+ public String toString() {
+ return "GoblintPosition{" +
+ "columnStart=" + columnStart +
+ ", columnEnd=" + columnEnd +
+ ", lineStart=" + lineStart +
+ ", lineEnd=" + lineEnd +
+ ", sourcefileURL=" + sourcefileURL +
+ '}';
+ }
}
diff --git a/src/test/java/GoblintMessagesTest.java b/src/test/java/GoblintMessagesTest.java
new file mode 100644
index 0000000..a425f2b
--- /dev/null
+++ b/src/test/java/GoblintMessagesTest.java
@@ -0,0 +1,283 @@
+import analysis.GoblintAnalysis;
+import analysis.GoblintCFGAnalysisResult;
+import analysis.GoblintMessagesAnalysisResult;
+import api.GoblintService;
+import api.json.GoblintMessageJsonHandler;
+import api.messages.GoblintAnalysisResult;
+import api.messages.GoblintFunctionsResult;
+import api.messages.GoblintMessagesResult;
+import api.messages.GoblintPosition;
+import api.messages.params.AnalyzeParams;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.ibm.wala.classLoader.Module;
+import com.ibm.wala.util.collections.Pair;
+import goblintserver.GoblintConfWatcher;
+import goblintserver.GoblintServer;
+import gobpie.GobPieConfiguration;
+import magpiebridge.core.AnalysisConsumer;
+import magpiebridge.core.AnalysisResult;
+import magpiebridge.core.MagpieServer;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.Spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+
+import static org.mockito.Mockito.*;
+
+public class GoblintMessagesTest {
+
+ private Gson gson;
+ @Mock
+ MagpieServer magpieServer = mock(MagpieServer.class);
+ @Mock
+ GoblintService goblintService = mock(GoblintService.class);
+ @Mock
+ GobPieConfiguration gobPieConfiguration = mock(GobPieConfiguration.class);
+ @Spy
+ GoblintServer goblintServer = spy(new GoblintServer(magpieServer, gobPieConfiguration));
+ @Mock
+ GoblintConfWatcher goblintConfWatcher = mock(GoblintConfWatcher.class);
+ GoblintAnalysis goblintAnalysis = new GoblintAnalysis(magpieServer, goblintServer, goblintService, gobPieConfiguration, goblintConfWatcher);
+ // Mock the arguments (files and analysisConsumer) for calling the GoblintAnalyze.analyze method
+ Collection extends Module> files = new ArrayDeque<>();
+ AnalysisConsumer analysisConsumer = mock(AnalysisConsumer.class);
+
+ /**
+ * Method to initialize gson to parse Goblint warnings from JSON.
+ */
+ private void createGsonBuilder() {
+ GoblintMessageJsonHandler goblintMessageJsonHandler = new GoblintMessageJsonHandler(new HashMap<>());
+ gson = goblintMessageJsonHandler.getDefaultGsonBuilder().create();
+ }
+
+ /**
+ * A function to mock that GoblintServer is alive
+ * and Goblint's configuration file is ok.
+ */
+ private void mockGoblintServerIsAlive(GoblintServer goblintServer) {
+ doReturn(true).when(goblintServer).isAlive();
+ when(goblintConfWatcher.refreshGoblintConfig()).thenReturn(true);
+ }
+
+ @BeforeEach
+ public void before() {
+ createGsonBuilder();
+ mockGoblintServerIsAlive(goblintServer);
+ // Mock that the command to execute is empty
+ when(gobPieConfiguration.getPreAnalyzeCommand()).thenReturn(new String[]{});
+ // Mock that the analyses of Goblint have started and completed
+ when(goblintService.analyze(new AnalyzeParams(false))).thenReturn(CompletableFuture.completedFuture(new GoblintAnalysisResult()));
+ // Mock that the incremental analysis is turned off (TODO: not sure why this is checked in reanalyze?)
+ when(gobPieConfiguration.useIncrementalAnalysis()).thenReturn(true);
+ }
+
+ // TODO: this can be generalized later to pass the file name as an argument
+ private List readGoblintResponseJson() throws IOException {
+ String messages = Files.readString(
+ Path.of(GoblintMessagesTest.class.getResource("messagesResponse.json").getPath())
+ );
+ return gson.fromJson(messages, new TypeToken>() {
+ }.getType());
+ }
+
+ private List readGoblintResponseJsonFunc() throws IOException {
+ String functions = Files.readString(
+ Path.of(GoblintMessagesTest.class.getResource("functionsResponse.json").getPath())
+ );
+ return gson.fromJson(functions, new TypeToken>() {
+ }.getType());
+ }
+
+
+ /**
+ * Mock test to ensure that the Goblint warnings received from a response in JSON format
+ * are correctly converted to {@link AnalysisResult} objects
+ * and passed to {@link MagpieServer} via {@link AnalysisConsumer}.
+ *
+ * @throws IOException when reading messagesResponse.json from resources fails.
+ */
+ @Test
+ public void testConvertMessagesFromJson() throws IOException {
+ List goblintMessagesResults = readGoblintResponseJson();
+ when(goblintService.messages()).thenReturn(CompletableFuture.completedFuture(goblintMessagesResults));
+ when(gobPieConfiguration.showCfg()).thenReturn(false);
+ goblintAnalysis.analyze(files, analysisConsumer, true);
+
+ URL emptyUrl = new File("").toURI().toURL();
+ GoblintPosition defaultPos = new GoblintPosition(1, 1, 1, 1, emptyUrl);
+ URL exampleUrl = new File("src/example.c").toURI().toURL();
+ List response = new ArrayList<>();
+ response.add(
+ new GoblintMessagesAnalysisResult(
+ defaultPos,
+ "[Deadcode] Logical lines of code (LLoC) summary",
+ "Info",
+ List.of(
+ Pair.make(defaultPos, "live: 12"),
+ Pair.make(defaultPos, "dead: 0"),
+ Pair.make(defaultPos, "total lines: 12")
+ )
+ )
+ );
+ response.add(
+ new GoblintMessagesAnalysisResult(
+ new GoblintPosition(4, 4, 4, 12, exampleUrl),
+ "[Race] Memory location myglobal (race with conf. 110)",
+ "Warning",
+ List.of(
+ Pair.make(
+ new GoblintPosition(10, 10, 2, 21, exampleUrl),
+ "write with [mhp:{tid=[main, t_fun@src/example.c:17:3-17:40#top]}, lock:{mutex1}, thread:[main, t_fun@src/example.c:17:3-17:40#top]] (conf. 110) (exp: & myglobal)"
+ ),
+ Pair.make(
+ new GoblintPosition(19, 19, 2, 21, exampleUrl),
+ "write with [mhp:{tid=[main]; created={[main, t_fun@src/example.c:17:3-17:40#top]}}, lock:{mutex2}, thread:[main]] (conf. 110) (exp: & myglobal)"
+ )
+ )
+ )
+ );
+ response.add(
+ new GoblintMessagesAnalysisResult(
+ defaultPos,
+ "[Race] Memory locations race summary",
+ "Info",
+ List.of(
+ Pair.make(defaultPos, "safe: 0"),
+ Pair.make(defaultPos, "vulnerable: 0"),
+ Pair.make(defaultPos, "unsafe: 1"),
+ Pair.make(defaultPos, "total memory locations: 1")
+ )
+ )
+ );
+ verify(analysisConsumer).consume(response, "GobPie");
+ }
+
+ /**
+ * Mock test to ensure that the Goblint warnings with Explode received from a response in JSON format
+ * are correctly converted to {@link AnalysisResult} objects
+ * and passed to {@link MagpieServer} via {@link AnalysisConsumer}.
+ *
+ * @throws IOException when reading messagesResponse.json from resources fails.
+ */
+ @Test
+ public void testConvertMessagesFromJsonWithExplode() throws IOException {
+ List goblintMessagesResults = readGoblintResponseJson();
+ when(goblintService.messages()).thenReturn(CompletableFuture.completedFuture(goblintMessagesResults));
+ when(gobPieConfiguration.showCfg()).thenReturn(false);
+ when(gobPieConfiguration.explodeGroupWarnings()).thenReturn(true);
+ goblintAnalysis.analyze(files, analysisConsumer, true);
+
+ URL emptyUrl = new File("").toURI().toURL();
+ GoblintPosition defaultPos = new GoblintPosition(1, 1, 1, 1, emptyUrl);
+ URL exampleUrl = new File("src/example.c").toURI().toURL();
+ List response = new ArrayList<>();
+ response.add(
+ new GoblintMessagesAnalysisResult(
+ defaultPos,
+ "[Deadcode] Logical lines of code (LLoC) summary",
+ "Info",
+ List.of(
+ Pair.make(defaultPos, "live: 12"),
+ Pair.make(defaultPos, "dead: 0"),
+ Pair.make(defaultPos, "total lines: 12")
+ )
+ )
+ );
+ response.add(
+ new GoblintMessagesAnalysisResult(
+ new GoblintPosition(10, 10, 2, 21, exampleUrl),
+ "[Race] Group: Memory location myglobal (race with conf. 110)\n" +
+ "write with [mhp:{tid=[main, t_fun@src/example.c:17:3-17:40#top]}, lock:{mutex1}, thread:[main, t_fun@src/example.c:17:3-17:40#top]] (conf. 110) (exp: & myglobal)",
+ "Warning",
+ List.of(Pair.make(
+ new GoblintPosition(19, 19, 2, 21, exampleUrl),
+ "write with [mhp:{tid=[main]; created={[main, t_fun@src/example.c:17:3-17:40#top]}}, lock:{mutex2}, thread:[main]] (conf. 110) (exp: & myglobal)"
+ )
+ )
+ )
+
+ );
+ response.add(
+ new GoblintMessagesAnalysisResult(
+ new GoblintPosition(19, 19, 2, 21, exampleUrl),
+ "[Race] Group: Memory location myglobal (race with conf. 110)\n" +
+ "write with [mhp:{tid=[main]; created={[main, t_fun@src/example.c:17:3-17:40#top]}}, lock:{mutex2}, thread:[main]] (conf. 110) (exp: & myglobal)",
+ "Warning",
+ List.of(Pair.make(
+ new GoblintPosition(10, 10, 2, 21, exampleUrl),
+ "write with [mhp:{tid=[main, t_fun@src/example.c:17:3-17:40#top]}, lock:{mutex1}, thread:[main, t_fun@src/example.c:17:3-17:40#top]] (conf. 110) (exp: & myglobal)"
+ )
+ )
+ )
+ );
+
+ response.add(
+ new GoblintMessagesAnalysisResult(
+ defaultPos,
+ "[Race] Memory locations race summary",
+ "Info",
+ List.of(
+ Pair.make(defaultPos, "safe: 0"),
+ Pair.make(defaultPos, "vulnerable: 0"),
+ Pair.make(defaultPos, "unsafe: 1"),
+ Pair.make(defaultPos, "total memory locations: 1")
+ )
+ )
+ );
+ verify(analysisConsumer).consume(response, "GobPie");
+
+ }
+
+ /**
+ * Mock test to ensure that the Goblint functions received from a response in JSON format
+ * are correctly converted to {@link GoblintCFGAnalysisResult} objects
+ * and passed to {@link MagpieServer} via {@link AnalysisConsumer}.
+ *
+ * @throws IOException when reading messagesResponse.json from resources fails.
+ */
+ @Test
+ public void testConvertFunctionsFromJson() throws IOException {
+ List goblintFunctionsResults = readGoblintResponseJsonFunc();
+ when(goblintService.functions()).thenReturn(CompletableFuture.completedFuture(goblintFunctionsResults));
+ when(gobPieConfiguration.showCfg()).thenReturn(true);
+ when(goblintService.messages()).thenReturn(CompletableFuture.completedFuture(new ArrayList<>()));
+ goblintAnalysis.analyze(files, analysisConsumer, true);
+
+ URL emptyUrl = new File("").toURI().toURL();
+ GoblintPosition defaultPos = new GoblintPosition(1, 1, 1, 1, emptyUrl);
+ URL exampleUrl = new File("src/example.c").toURI().toURL();
+ List response = new ArrayList<>();
+ response.add(
+ new GoblintCFGAnalysisResult(
+ new GoblintPosition(8, 13, 0, 0, exampleUrl),
+ "show cfg",
+ "t_fun"
+ )
+ );
+ response.add(
+ new GoblintCFGAnalysisResult(
+ new GoblintPosition(15, 23, 0, 0, exampleUrl),
+ "show arg",
+ ""
+ )
+ );
+ response.add(
+ new GoblintCFGAnalysisResult(
+ new GoblintPosition(15, 23, 0, 0, exampleUrl),
+ "show cfg",
+ "main"
+ )
+ );
+ verify(analysisConsumer).consume(response, "GobPie");
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/resources/functionsResponse.json b/src/test/resources/functionsResponse.json
new file mode 100644
index 0000000..ba48e01
--- /dev/null
+++ b/src/test/resources/functionsResponse.json
@@ -0,0 +1,27 @@
+[
+ {
+ "funName": "t_fun",
+ "location": {
+ "file": "src/example.c",
+ "line": 8,
+ "column": 1,
+ "byte": 52058,
+ "endLine": 13,
+ "endColumn": 1,
+ "endByte": 52264
+ }
+ },
+ {
+ "funName": "main",
+ "location": {
+ "file": "src/example.c",
+ "line": 15,
+ "column": 1,
+ "byte": 52267,
+ "endLine": 23,
+ "endColumn": 1,
+ "endByte": 52805
+ }
+ }
+]
+
diff --git a/src/test/resources/messagesResponse.json b/src/test/resources/messagesResponse.json
new file mode 100644
index 0000000..3e64118
--- /dev/null
+++ b/src/test/resources/messagesResponse.json
@@ -0,0 +1,119 @@
+[
+ {
+ "tags": [
+ {
+ "Category": [
+ "Deadcode"
+ ]
+ }
+ ],
+ "severity": "Info",
+ "multipiece": {
+ "group_text": "Logical lines of code (LLoC) summary",
+ "group_loc": null,
+ "pieces": [
+ {
+ "loc": null,
+ "text": "live: 12",
+ "context": null
+ },
+ {
+ "loc": null,
+ "text": "dead: 0",
+ "context": null
+ },
+ {
+ "loc": null,
+ "text": "total lines: 12",
+ "context": null
+ }
+ ]
+ }
+ },
+ {
+ "tags": [
+ {
+ "Category": [
+ "Race"
+ ]
+ }
+ ],
+ "severity": "Warning",
+ "multipiece": {
+ "group_text": "Memory location myglobal (race with conf. 110)",
+ "group_loc": {
+ "file": "src/example.c",
+ "line": 4,
+ "column": 5,
+ "byte": 51631,
+ "endLine": 4,
+ "endColumn": 13,
+ "endByte": 51639
+ },
+ "pieces": [
+ {
+ "loc": {
+ "file": "src/example.c",
+ "line": 10,
+ "column": 3,
+ "byte": 52116,
+ "endLine": 10,
+ "endColumn": 22,
+ "endByte": 52135
+ },
+ "text": "write with [mhp:{tid=[main, t_fun@src/example.c:17:3-17:40#top]}, lock:{mutex1}, thread:[main, t_fun@src/example.c:17:3-17:40#top]] (conf. 110) (exp: & myglobal)",
+ "context": null
+ },
+ {
+ "loc": {
+ "file": "src/example.c",
+ "line": 19,
+ "column": 3,
+ "byte": 52611,
+ "endLine": 19,
+ "endColumn": 22,
+ "endByte": 52630
+ },
+ "text": "write with [mhp:{tid=[main]; created={[main, t_fun@src/example.c:17:3-17:40#top]}}, lock:{mutex2}, thread:[main]] (conf. 110) (exp: & myglobal)",
+ "context": null
+ }
+ ]
+ }
+ },
+ {
+ "tags": [
+ {
+ "Category": [
+ "Race"
+ ]
+ }
+ ],
+ "severity": "Info",
+ "multipiece": {
+ "group_text": "Memory locations race summary",
+ "group_loc": null,
+ "pieces": [
+ {
+ "loc": null,
+ "text": "safe: 0",
+ "context": null
+ },
+ {
+ "loc": null,
+ "text": "vulnerable: 0",
+ "context": null
+ },
+ {
+ "loc": null,
+ "text": "unsafe: 1",
+ "context": null
+ },
+ {
+ "loc": null,
+ "text": "total memory locations: 1",
+ "context": null
+ }
+ ]
+ }
+ }
+]
\ No newline at end of file