-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathCodeAnalyzerIntegrationTest.java
More file actions
395 lines (351 loc) · 21.8 KB
/
CodeAnalyzerIntegrationTest.java
File metadata and controls
395 lines (351 loc) · 21.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
package com.ibm.cldk;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Assertions;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.MountableFile;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.Map;
import java.util.Properties;
import java.util.stream.StreamSupport;
@Testcontainers
@SuppressWarnings("resource")
public class CodeAnalyzerIntegrationTest {
/**
* Creates a Java 11 test container that mounts the build/libs folder.
*/
static String codeanalyzerVersion;
static final String javaVersion = "17";
static String javaHomePath;
static {
// Build project first
try {
Process process = new ProcessBuilder("./gradlew", "fatJar")
.directory(new File(System.getProperty("user.dir")))
.start();
if (process.waitFor() != 0) {
throw new RuntimeException("Build failed");
}
} catch (IOException | InterruptedException e) {
throw new RuntimeException("Failed to build codeanalyzer", e);
}
}
@Container
static final GenericContainer<?> container = new GenericContainer<>("ubuntu:latest")
.withCreateContainerCmdModifier(cmd -> cmd.withEntrypoint("sh"))
.withCommand("-c", "while true; do sleep 1; done")
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("build/libs")), "/opt/jars")
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("build/libs")), "/opt/jars")
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("src/test/resources/test-applications/mvnw-corrupt-test")), "/test-applications/mvnw-corrupt-test")
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("src/test/resources/test-applications/plantsbywebsphere")), "/test-applications/plantsbywebsphere")
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("src/test/resources/test-applications/call-graph-test")), "/test-applications/call-graph-test")
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("src/test/resources/test-applications/record-class-test")), "/test-applications/record-class-test")
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("src/test/resources/test-applications/init-blocks-test")), "/test-applications/init-blocks-test")
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("src/test/resources/test-applications/mvnw-working-test")), "/test-applications/mvnw-working-test");
@Container
static final GenericContainer<?> mavenContainer = new GenericContainer<>("maven:3.8.3-openjdk-17")
.withCreateContainerCmdModifier(cmd -> cmd.withEntrypoint("sh"))
.withCommand("-c", "while true; do sleep 1; done")
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("build/libs")), "/opt/jars")
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("src/test/resources/test-applications/mvnw-corrupt-test")), "/test-applications/mvnw-corrupt-test")
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("src/test/resources/test-applications/mvnw-working-test")), "/test-applications/mvnw-working-test")
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("src/test/resources/test-applications/daytrader8")), "/test-applications/daytrader8");
public CodeAnalyzerIntegrationTest() throws IOException, InterruptedException {
}
@BeforeAll
static void setUp() {
// Install Java 17 in the base container
try {
container.execInContainer("apt-get", "update");
container.execInContainer("apt-get", "install", "-y", "openjdk-17-jdk");
// Get JAVA_HOME dynamically
var javaHomeResult = container.execInContainer("bash", "-c",
"dirname $(dirname $(readlink -f $(which java)))"
);
javaHomePath = javaHomeResult.getStdout().trim();
Assertions.assertFalse(javaHomePath.isEmpty(), "Failed to determine JAVA_HOME");
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
// Get the version of the codeanalyzer jar
Properties properties = new Properties();
try (FileInputStream fis = new FileInputStream(
Paths.get(System.getProperty("user.dir"), "gradle.properties").toFile())) {
properties.load(fis);
} catch (IOException e) {
throw new RuntimeException(e);
}
codeanalyzerVersion = properties.getProperty("version");
}
@Test
void shouldHaveCorrectJavaVersionInstalled() throws Exception {
var baseContainerresult = container.execInContainer("java", "-version");
var mvnContainerresult = mavenContainer.execInContainer("java", "-version");
Assertions.assertTrue(baseContainerresult.getStderr().contains("openjdk version \"" + javaVersion), "Base container Java version should be " + javaVersion);
Assertions.assertTrue(mvnContainerresult.getStderr().contains("openjdk version \"" + javaVersion), "Maven container Java version should be " + javaVersion);
}
@Test
void shouldHaveCodeAnalyzerJar() throws Exception {
var dirContents = container.execInContainer("ls", "/opt/jars/");
Assertions.assertTrue(dirContents.getStdout().length() > 0, "Directory listing should not be empty");
Assertions.assertTrue(dirContents.getStdout().contains("codeanalyzer"), "Codeanalyzer.jar not found in the container.");
}
@Test
void shouldBeAbleToRunCodeAnalyzer() throws Exception {
var runCodeAnalyzerJar = container.execInContainer(
"bash", "-c",
String.format("export JAVA_HOME=%s && java -jar /opt/jars/codeanalyzer-%s.jar --help",
javaHomePath, codeanalyzerVersion
));
Assertions.assertEquals(0, runCodeAnalyzerJar.getExitCode(),
"Command should execute successfully");
Assertions.assertTrue(runCodeAnalyzerJar.getStdout().length() > 0,
"Should have some output");
}
@Test
void callGraphShouldHaveKnownEdges() throws Exception {
var runCodeAnalyzerOnCallGraphTest = container.execInContainer(
"bash", "-c",
String.format(
"export JAVA_HOME=%s && java -jar /opt/jars/codeanalyzer-%s.jar --input=/test-applications/call-graph-test --analysis-level=2",
javaHomePath, codeanalyzerVersion
)
);
// Read the output JSON
Gson gson = new Gson();
JsonObject jsonObject = gson.fromJson(runCodeAnalyzerOnCallGraphTest.getStdout(), JsonObject.class);
JsonArray callGraph = jsonObject.getAsJsonArray("call_graph");
Assertions.assertTrue(StreamSupport.stream(callGraph.spliterator(), false)
.map(JsonElement::getAsJsonObject)
.anyMatch(entry ->
"CALL_DEP".equals(entry.get("type").getAsString()) &&
"1".equals(entry.get("weight").getAsString()) &&
entry.getAsJsonObject("source").get("signature").getAsString().equals("helloString()") &&
entry.getAsJsonObject("target").get("signature").getAsString().equals("log()")
), "Expected edge not found in the system dependency graph");
}
@Test
void corruptMavenShouldNotBuildWithWrapper() throws IOException, InterruptedException {
// Make executable
mavenContainer.execInContainer("chmod", "+x", "/test-applications/mvnw-corrupt-test/mvnw");
// Let's start by building the project by itself
var mavenProjectBuildWithWrapper = mavenContainer.withWorkingDirectory("/test-applications/mvnw-corrupt-test").execInContainer("/test-applications/mvnw-corrupt-test/mvnw", "clean", "compile");
Assertions.assertNotEquals(0, mavenProjectBuildWithWrapper.getExitCode());
}
@Test
void corruptMavenShouldProduceAnalysisArtifactsWhenMVNCommandIsInPath() throws IOException, InterruptedException {
// Let's start by building the project by itself
var corruptMavenProjectBuild = mavenContainer.withWorkingDirectory("/test-applications/mvnw-corrupt-test").execInContainer("mvn", "-f", "/test-applications/mvnw-corrupt-test/pom.xml", "clean", "compile");
Assertions.assertEquals(0, corruptMavenProjectBuild.getExitCode(), "Failed to build the project with system's default Maven.");
// NOw run codeanalyzer and assert if analysis.json is generated.
var runCodeAnalyzer = mavenContainer.execInContainer("java", "-jar", String.format("/opt/jars/codeanalyzer-%s.jar", codeanalyzerVersion), "--input=/test-applications/mvnw-corrupt-test", "--output=/tmp/", "--analysis-level=2", "--verbose", "--no-build");
var codeAnalyzerOutputDirContents = mavenContainer.execInContainer("ls", "/tmp/analysis.json");
String codeAnalyzerOutputDirContentsStdOut = codeAnalyzerOutputDirContents.getStdout();
Assertions.assertTrue(codeAnalyzerOutputDirContentsStdOut.length() > 0, "Could not find 'analysis.json'.");
// mvnw is corrupt, so we should see an error message in the output.
Assertions.assertTrue(runCodeAnalyzer.getStdout().contains("[ERROR]\tCannot run program \"/test-applications/mvnw-corrupt-test/mvnw\"") && runCodeAnalyzer.getStdout().contains("/mvn."));
// We should correctly identify the build tool used in the mvn command from the system path.
Assertions.assertTrue(runCodeAnalyzer.getStdout().contains("[INFO]\tBuilding the project using /usr/bin/mvn."));
}
@Test
void corruptMavenShouldNotTerminateWithErrorWhenMavenIsNotPresentUnlessAnalysisLevel2() throws IOException, InterruptedException {
// When analysis level 2, we should get a Runtime Exception
var runCodeAnalyzer = container.execInContainer(
"bash", "-c",
String.format(
"export JAVA_HOME=%s && java -jar /opt/jars/codeanalyzer-%s.jar --input=/test-applications/mvnw-corrupt-test --output=/tmp/ --analysis-level=2",
javaHomePath, codeanalyzerVersion
)
);
Assertions.assertEquals(1, runCodeAnalyzer.getExitCode());
Assertions.assertTrue(runCodeAnalyzer.getStderr().contains("java.lang.RuntimeException"));
}
@Test
void shouldBeAbleToGenerateAnalysisArtifactForDaytrader8() throws Exception {
var runCodeAnalyzerOnDaytrader8 = mavenContainer.execInContainer(
"bash", "-c",
String.format(
"export JAVA_HOME=%s && java -jar /opt/jars/codeanalyzer-%s.jar --input=/test-applications/daytrader8 --analysis-level=1",
javaHomePath, codeanalyzerVersion
)
);
Assertions.assertTrue(runCodeAnalyzerOnDaytrader8.getStdout().contains("\"is_entrypoint_class\": true"), "No entry point classes found");
Assertions.assertTrue(runCodeAnalyzerOnDaytrader8.getStdout().contains("\"is_entrypoint\": true"), "No entry point methods found");
}
@Test
void shouldBeAbleToDetectCRUDOperationsAndQueriesForPlantByWebsphere() throws Exception {
var runCodeAnalyzerOnPlantsByWebsphere = container.execInContainer(
"bash", "-c",
String.format(
"export JAVA_HOME=%s && java -jar /opt/jars/codeanalyzer-%s.jar --input=/test-applications/plantsbywebsphere --analysis-level=1 --verbose",
javaHomePath, codeanalyzerVersion
)
);
String output = runCodeAnalyzerOnPlantsByWebsphere.getStdout();
Assertions.assertTrue(output.contains("\"query_type\": \"NAMED\""), "No entry point classes found");
Assertions.assertTrue(output.contains("\"operation_type\": \"READ\""), "No entry point methods found");
Assertions.assertTrue(output.contains("\"operation_type\": \"UPDATE\""), "No entry point methods found");
Assertions.assertTrue(output.contains("\"operation_type\": \"CREATE\""), "No entry point methods found");
// Convert the expected JSON structure into a string
String expectedCrudOperation =
"\"crud_operations\": [" +
"{" +
"\"line_number\": 115," +
"\"operation_type\": \"READ\"," +
"\"target_table\": null," +
"\"involved_columns\": null," +
"\"condition\": null," +
"\"joined_tables\": null" +
"}]";
// Expected JSON for CRUD Queries
String expectedCrudQuery =
"\"crud_queries\": [" +
"{" +
"\"line_number\": 141,";
// Normalize the output and expected strings to ignore formatting differences
String normalizedOutput = output.replaceAll("\\s+", "");
String normalizedExpectedCrudOperation = expectedCrudOperation.replaceAll("\\s+", "");
String normalizedExpectedCrudQuery = expectedCrudQuery.replaceAll("\\s+", "");
// Assertions for both CRUD operations and queries
Assertions.assertTrue(normalizedOutput.contains(normalizedExpectedCrudOperation), "Expected CRUD operation JSON structure not found");
Assertions.assertTrue(normalizedOutput.contains(normalizedExpectedCrudQuery), "Expected CRUD query JSON structure not found");
}
@Test
void symbolTableShouldHaveRecords() throws IOException, InterruptedException {
var runCodeAnalyzerOnCallGraphTest = container.execInContainer(
"bash", "-c",
String.format(
"export JAVA_HOME=%s && java -jar /opt/jars/codeanalyzer-%s.jar --input=/test-applications/record-class-test --analysis-level=1",
javaHomePath, codeanalyzerVersion
)
);
// Read the output JSON
Gson gson = new Gson();
JsonObject jsonObject = gson.fromJson(runCodeAnalyzerOnCallGraphTest.getStdout(), JsonObject.class);
JsonObject symbolTable = jsonObject.getAsJsonObject("symbol_table");
Assertions.assertEquals(4, symbolTable.size(), "Symbol table should have 4 records");
}
@Test
void symbolTableShouldHaveDefaultRecordComponents() throws IOException, InterruptedException {
var runCodeAnalyzerOnCallGraphTest = container.execInContainer(
"bash", "-c",
String.format(
"export JAVA_HOME=%s && java -jar /opt/jars/codeanalyzer-%s.jar --input=/test-applications/record-class-test --analysis-level=1",
javaHomePath, codeanalyzerVersion
)
);
// Read the output JSON
Gson gson = new Gson();
JsonObject jsonObject = gson.fromJson(runCodeAnalyzerOnCallGraphTest.getStdout(), JsonObject.class);
JsonObject symbolTable = jsonObject.getAsJsonObject("symbol_table");
for (Map.Entry<String, JsonElement> element : symbolTable.entrySet()) {
String key = element.getKey();
if (!key.endsWith("PersonRecord.java")) {
continue;
}
JsonObject type = element.getValue().getAsJsonObject();
if (type.has("type_declarations")) {
JsonObject typeDeclarations = type.getAsJsonObject("type_declarations");
JsonArray recordComponent = typeDeclarations.getAsJsonObject("org.example.PersonRecord").getAsJsonArray("record_components");
Assertions.assertEquals(2, recordComponent.size(), "Record component should have 2 components");
JsonObject record = recordComponent.get(1).getAsJsonObject();
Assertions.assertTrue(record.get("name").getAsString().equals("age") && record.get("default_value").getAsInt() == 18, "Record component should have a name");
}
}
}
@Test
void parametersInCallableMustHaveStartAndEndLineAndColumns() throws IOException, InterruptedException {
var runCodeAnalyzerOnCallGraphTest = container.execInContainer(
"bash", "-c",
String.format(
"export JAVA_HOME=%s && java -jar /opt/jars/codeanalyzer-%s.jar --input=/test-applications/record-class-test --analysis-level=1",
javaHomePath, codeanalyzerVersion
)
);
// Read the output JSON
Gson gson = new Gson();
JsonObject jsonObject = gson.fromJson(runCodeAnalyzerOnCallGraphTest.getStdout(), JsonObject.class);
JsonObject symbolTable = jsonObject.getAsJsonObject("symbol_table");
for (Map.Entry<String, JsonElement> element : symbolTable.entrySet()) {
String key = element.getKey();
if (!key.endsWith("App.java")) {
continue;
}
JsonObject type = element.getValue().getAsJsonObject();
if (type.has("type_declarations")) {
JsonObject typeDeclarations = type.getAsJsonObject("type_declarations");
JsonObject mainMethod = typeDeclarations.getAsJsonObject("org.example.App").getAsJsonObject("callable_declarations").getAsJsonObject("main(String[])");
JsonArray parameters = mainMethod.getAsJsonArray("parameters");
// There should be 1 parameter
Assertions.assertEquals(1, parameters.size(), "Callable should have 1 parameter");
JsonObject parameter = parameters.get(0).getAsJsonObject();
// Start and end line and column should not be -1
Assertions.assertTrue(parameter.get("start_line").getAsInt() == 7 && parameter.get("end_line").getAsInt() == 7 && parameter.get("start_column").getAsInt() == 29 && parameter.get("end_column").getAsInt() == 41, "Parameter should have start and end line and columns");
}
}
}
@Test
void mustBeAbleToResolveInitializationBlocks() throws IOException, InterruptedException {
var runCodeAnalyzerOnCallGraphTest = container.execInContainer(
"bash", "-c",
String.format(
"export JAVA_HOME=%s && java -jar /opt/jars/codeanalyzer-%s.jar --input=/test-applications/init-blocks-test --analysis-level=1",
javaHomePath, codeanalyzerVersion
)
);
// Read the output JSON
Gson gson = new Gson();
JsonObject jsonObject = gson.fromJson(runCodeAnalyzerOnCallGraphTest.getStdout(), JsonObject.class);
JsonObject symbolTable = jsonObject.getAsJsonObject("symbol_table");
for (Map.Entry<String, JsonElement> element : symbolTable.entrySet()) {
String key = element.getKey();
if (!key.endsWith("App.java")) {
continue;
}
JsonObject type = element.getValue().getAsJsonObject();
if (type.has("type_declarations")) {
JsonObject typeDeclarations = type.getAsJsonObject("type_declarations");
JsonArray initializationBlocks = typeDeclarations.getAsJsonObject("org.example.App").getAsJsonArray("initialization_blocks");
// There should be 2 blocks
Assertions.assertEquals(2, initializationBlocks.size(), "Callable should have 1 parameter");
Assertions.assertTrue(initializationBlocks.get(0).getAsJsonObject().get("is_static").getAsBoolean(), "Static block should be marked as static");
Assertions.assertFalse(initializationBlocks.get(1).getAsJsonObject().get("is_static").getAsBoolean(), "Instance block should be marked as not static");
}
}
}
@Test
void mustBeAbleToExtractCommentBlocks() throws IOException, InterruptedException {
var runCodeAnalyzerOnCallGraphTest = container.execInContainer(
"bash", "-c",
String.format(
"export JAVA_HOME=%s && java -jar /opt/jars/codeanalyzer-%s.jar --input=/test-applications/init-blocks-test --analysis-level=1",
javaHomePath, codeanalyzerVersion
)
);
// Read the output JSON
Gson gson = new Gson();
JsonObject jsonObject = gson.fromJson(runCodeAnalyzerOnCallGraphTest.getStdout(), JsonObject.class);
JsonObject symbolTable = jsonObject.getAsJsonObject("symbol_table");
for (Map.Entry<String, JsonElement> element : symbolTable.entrySet()) {
String key = element.getKey();
if (!key.endsWith("App.java")) {
continue;
}
JsonObject type = element.getValue().getAsJsonObject();
JsonArray comments = type.getAsJsonArray("comments");
Assertions.assertEquals(16, comments.size(), "Should have 15 comments");
Assertions.assertTrue(StreamSupport.stream(comments.spliterator(), false)
.map(JsonElement::getAsJsonObject)
.anyMatch(comment -> comment.get("is_javadoc").getAsBoolean()), "Single line comment not found");
}
}
}