Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions client-java/controller/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,12 @@
<artifactId>lettuce-core</artifactId>
<scope>test</scope>
</dependency>
<!-- Bringing Netty dependency explicitly as it was excluded from spring-boot-starter-data-redis to avoid version conflict -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>netty-nio-client</artifactId>
<scope>test</scope>
</dependency>

<!--gRPC RPC test-->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ public enum ReplacementCategory {
/**
* Replacements to handle NEO4J command interceptions
*/
NEO4J
NEO4J,

/**
* Replacements to handle DYNAMODB command interceptions
*/
DYNAMODB
}

19 changes: 18 additions & 1 deletion client-java/instrumentation/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
<packaging>jar</packaging>
<name>${project.groupId}:${project.artifactId}</name>


<dependencies>
<dependency>
<groupId>org.evomaster</groupId>
Expand Down Expand Up @@ -72,6 +71,13 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>${springboot.version}</version>
<!-- Need to exclude Netty from this package to avoid a clash of versions with AWS SDK -->
<exclusions>
<exclusion>
<groupId>io.netty</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
<scope>test</scope>
</dependency>
<!-- <dependency>-->
Expand All @@ -87,6 +93,17 @@
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
</dependency>
<!-- DynamoDB, need to bring Netty explicitly to avoid conflicts from the version coming from Lettuce -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>dynamodb</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>netty-nio-client</artifactId>
<scope>test</scope>
</dependency>

<!-- Dependencies for Java Regex distances -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ public StatementDescription(String line, String method) {

private final Set<RedisCommand> redisCommandData = new CopyOnWriteArraySet<>();

private final Set<DynamoDbCommand> dynamoDbInfoData = new CopyOnWriteArraySet<>();

private final Set<MongoCollectionSchema> mongoCollectionSchemaData = new CopyOnWriteArraySet<>();

public Set<ExecutedSqlCommand> getSqlInfoData(){
Expand All @@ -134,6 +136,10 @@ public Set<RedisCommand> getRedisCommandData(){
return Collections.unmodifiableSet(redisCommandData);
}

public Set<DynamoDbCommand> getDynamoDbInfoData(){
return Collections.unmodifiableSet(dynamoDbInfoData);
}

public Set<MongoCollectionSchema> getMongoCollectionTypeData(){
return Collections.unmodifiableSet(mongoCollectionSchemaData);
}
Expand All @@ -158,6 +164,10 @@ public void addRedisCommand(RedisCommand info){
redisCommandData.add(info);
}

public void addDynamoDbInfo(DynamoDbCommand info){
dynamoDbInfoData.add(info);
}

public void addMongoCollectionType(MongoCollectionSchema mongoCollectionSchema){
mongoCollectionSchemaData.add(mongoCollectionSchema);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package org.evomaster.client.java.instrumentation;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
* Info related to DynamoDB command execution.
*/
public class DynamoDbCommand implements Serializable {


/**
* Names of the tables the operation was applied to (with Batch operations, more than one table is possible)
*/
private final List<String> tableNames;
/**
* Name of the operation that was executed
*/
private final String operationName;
/**
* Actual executed operation
*/
private final Object request;
/**
* If the operation was successfully executed
*/
private final boolean successfullyExecuted;
/**
* Elapsed execution time
*/
private final long executionTime;

public DynamoDbCommand(List<String> tableNames, String operationName, Object request, boolean successfullyExecuted, long executionTime) {
this.tableNames = tableNames == null
? Collections.emptyList()
: Collections.unmodifiableList(new ArrayList<>(tableNames));
this.operationName = operationName;
this.request = request;
this.successfullyExecuted = successfullyExecuted;
this.executionTime = executionTime;
}

public List<String> getTableNames() {
return tableNames;
}

public String getOperationName() {
return operationName;
}

public Object getRequest() {
return request;
}

public boolean isSuccessfullyExecuted() {
return successfullyExecuted;
}

public long getExecutionTime() {
return executionTime;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public static List<MethodReplacementClass> getList() {
new DateFormatClassReplacement(),
new DocumentClassReplacement(),
new DoubleClassReplacement(),
new DynamoDbClassReplacement.Sync(),
new DynamoDbClassReplacement.Async(),
new EnumClassReplacement(),
new FloatClassReplacement(),
new GsonClassReplacement(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package org.evomaster.client.java.instrumentation.coverage.methodreplacement.thirdpartyclasses;

import org.evomaster.client.java.instrumentation.DynamoDbCommand;
import org.evomaster.client.java.instrumentation.coverage.methodreplacement.Replacement;
import org.evomaster.client.java.instrumentation.coverage.methodreplacement.ThirdPartyCast;
import org.evomaster.client.java.instrumentation.coverage.methodreplacement.ThirdPartyMethodReplacementClass;
import org.evomaster.client.java.instrumentation.coverage.methodreplacement.UsageFilter;
import org.evomaster.client.java.instrumentation.shared.ReplacementCategory;
import org.evomaster.client.java.instrumentation.shared.ReplacementType;
import org.evomaster.client.java.instrumentation.staticstate.ExecutionTracer;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.concurrent.CompletableFuture;

import static org.evomaster.client.java.instrumentation.coverage.methodreplacement.ThirdPartyMethodReplacementClass.getOriginal;

/**
* Made the decision to add both Sync and Async replacements in one class. They are mostly the same but I couldn't find a better way with static and annotations
*/
public class DynamoDbClassReplacement {

public static class Sync extends ThirdPartyMethodReplacementClass {
public static final String DDB_GET_ITEM = "ddbGetItem";
public static final String DDB_BATCH_GET_ITEM = "ddbBatchGetItem";
public static final String DDB_PUT_ITEM = "ddbPutItem";
public static final String DDB_UPDATE_ITEM = "ddbUpdateItem";
public static final String DDB_DELETE_ITEM = "ddbDeleteItem";
public static final String DDB_QUERY = "ddbQuery";
public static final String DDB_SCAN = "ddbScan";
private static final Sync singleton = new Sync();

@Override
protected String getNameOfThirdPartyTargetClass() {
return "software.amazon.awssdk.services.dynamodb.DynamoDbClient";
}

@Replacement(type = ReplacementType.TRACKER, id = DDB_GET_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "software.amazon.awssdk.services.dynamodb.model.GetItemResponse")
public static Object getItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.GetItemRequest") Object request) {
return handle(singleton, client, DDB_GET_ITEM, request, "GetItem");
}

@Replacement(type = ReplacementType.TRACKER, id = DDB_BATCH_GET_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "software.amazon.awssdk.services.dynamodb.model.BatchGetItemResponse")
public static Object batchGetItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.BatchGetItemRequest") Object request) {
return handle(singleton, client, DDB_BATCH_GET_ITEM, request, "BatchGetItem");
}

@Replacement(type = ReplacementType.TRACKER, id = DDB_PUT_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "software.amazon.awssdk.services.dynamodb.model.PutItemResponse")
public static Object putItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.PutItemRequest") Object request) {
return handle(singleton, client, DDB_PUT_ITEM, request, "PutItem");
}

@Replacement(type = ReplacementType.TRACKER, id = DDB_UPDATE_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "software.amazon.awssdk.services.dynamodb.model.UpdateItemResponse")
public static Object updateItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest") Object request) {
return handle(singleton, client, DDB_UPDATE_ITEM, request, "UpdateItem");
}

@Replacement(type = ReplacementType.TRACKER, id = DDB_DELETE_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "software.amazon.awssdk.services.dynamodb.model.DeleteItemResponse")
public static Object deleteItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest") Object request) {
return handle(singleton, client, DDB_DELETE_ITEM, request, "DeleteItem");
}

@Replacement(type = ReplacementType.TRACKER, id = DDB_QUERY, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "software.amazon.awssdk.services.dynamodb.model.QueryResponse")
public static Object query(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.QueryRequest") Object request) {
return handle(singleton, client, DDB_QUERY, request, "Query");
}

@Replacement(type = ReplacementType.TRACKER, id = DDB_SCAN, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "software.amazon.awssdk.services.dynamodb.model.ScanResponse")
public static Object scan(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.ScanRequest") Object request) {
return handle(singleton, client, DDB_SCAN, request, "Scan");
}
}

public static class Async extends ThirdPartyMethodReplacementClass {
public static final String DDB_ASYNC_GET_ITEM = "ddbAsyncGetItem";
public static final String DDB_ASYNC_BATCH_GET_ITEM = "ddbAsyncBatchGetItem";
public static final String DDB_ASYNC_PUT_ITEM = "ddbAsyncPutItem";
public static final String DDB_ASYNC_UPDATE_ITEM = "ddbAsyncUpdateItem";
public static final String DDB_ASYNC_DELETE_ITEM = "ddbAsyncDeleteItem";
public static final String DDB_ASYNC_QUERY = "ddbAsyncQuery";
public static final String DDB_ASYNC_SCAN = "ddbAsyncScan";
private static final Async singleton = new Async();

@Override
protected String getNameOfThirdPartyTargetClass() {
return "software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient";
}

@Replacement(type = ReplacementType.TRACKER, id = DDB_ASYNC_GET_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "java.util.concurrent.CompletableFuture")
public static Object getItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.GetItemRequest") Object request) {
return handle(singleton, client, DDB_ASYNC_GET_ITEM, request, "GetItem");
}

@Replacement(type = ReplacementType.TRACKER, id = DDB_ASYNC_BATCH_GET_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "java.util.concurrent.CompletableFuture")
public static Object batchGetItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.BatchGetItemRequest") Object request) {
return handle(singleton, client, DDB_ASYNC_BATCH_GET_ITEM, request, "BatchGetItem");
}

@Replacement(type = ReplacementType.TRACKER, id = DDB_ASYNC_PUT_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "java.util.concurrent.CompletableFuture")
public static Object putItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.PutItemRequest") Object request) {
return handle(singleton, client, DDB_ASYNC_PUT_ITEM, request, "PutItem");
}

@Replacement(type = ReplacementType.TRACKER, id = DDB_ASYNC_UPDATE_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "java.util.concurrent.CompletableFuture")
public static Object updateItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest") Object request) {
return handle(singleton, client, DDB_ASYNC_UPDATE_ITEM, request, "UpdateItem");
}

@Replacement(type = ReplacementType.TRACKER, id = DDB_ASYNC_DELETE_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "java.util.concurrent.CompletableFuture")
public static Object deleteItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest") Object request) {
return handle(singleton, client, DDB_ASYNC_DELETE_ITEM, request, "DeleteItem");
}

@Replacement(type = ReplacementType.TRACKER, id = DDB_ASYNC_QUERY, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "java.util.concurrent.CompletableFuture")
public static Object query(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.QueryRequest") Object request) {
return handle(singleton, client, DDB_ASYNC_QUERY, request, "Query");
}

@Replacement(type = ReplacementType.TRACKER, id = DDB_ASYNC_SCAN, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "java.util.concurrent.CompletableFuture")
public static Object scan(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.ScanRequest") Object request) {
return handle(singleton, client, DDB_ASYNC_SCAN, request, "Scan");
}
}

protected static Object handle(ThirdPartyMethodReplacementClass singleton, Object client, String id, Object request, String operationName) {
long start = System.currentTimeMillis();
boolean isAsync = client.getClass().getName().contains("Async");
Comment thread
aschenzle marked this conversation as resolved.
Outdated
try {
Method method = getOriginal(singleton, id, client);
Object result = method.invoke(client, request);

if (isAsync) {
CompletableFuture<?> future = (CompletableFuture<?>) result;
return future.handle((res, ex) -> {
long end = System.currentTimeMillis();
List<String> tableNames = extractTableNames(request);
boolean successful = ex == null;
long executionTime = end - start;
DynamoDbCommand info = new DynamoDbCommand(tableNames, operationName, request, successful, executionTime);
ExecutionTracer.addDynamoDbInfo(info);
if (ex != null) {
if (ex instanceof RuntimeException) throw (RuntimeException) ex;
throw new RuntimeException(ex);
}
return res;
});
} else {
long end = System.currentTimeMillis();
List<String> tableNames = extractTableNames(request);
long executionTime = end - start;
DynamoDbCommand info = new DynamoDbCommand(tableNames, operationName, request, true, executionTime);
ExecutionTracer.addDynamoDbInfo(info);
return result;
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e.getCause());
}
}

private static List<String> extractTableNames(Object request) {
if (request == null) return Collections.emptyList();

try {
Method getTableNameMethod = request.getClass().getMethod("tableName");
String tableName = (String) getTableNameMethod.invoke(request);
if (tableName != null) {
return Collections.singletonList(tableName);
}
} catch (NoSuchMethodException ignored) {
// no-op
Comment thread
aschenzle marked this conversation as resolved.
Outdated
} catch (IllegalAccessException | InvocationTargetException e) {
return Collections.emptyList();
}

try {
Method getRequestItemsMethod = request.getClass().getMethod("requestItems");
Object requestItems = getRequestItemsMethod.invoke(request);
if (!(requestItems instanceof Map)) {
return Collections.emptyList();
}

List<String> tableNames = new ArrayList<>();
for (Object key : ((Map<?, ?>) requestItems).keySet()) {
if (key instanceof String) {
tableNames.add((String) key);
}
}

Collections.sort(tableNames);
return tableNames;
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
return Collections.emptyList();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is is defaulted to empty?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seemed like the best option, as we don't want the interception to throw exceptions and retuning null requires handling it. We are just getting less metadata about the interception for this particular request if for some weird reason it fails (Only case I can think this failing would be a dependency problem with another version of the SDK breaking API compatibility, not expected from AWS).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't you encapsulate this and throw a RuntimeException?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just committed a refactored version.

}
}
}
Loading