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 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 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 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