Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package org.evomaster.client.java.controller.dynamodb;

import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbAttributeValueDto;
import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbInsertionDto;
import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbInsertionResultsDto;

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

/**
* Executes DynamoDB insertions without binding the controller API to an AWS SDK version.
*/
public final class DynamoDbCommandExecutor {

private DynamoDbCommandExecutor() {
}

/**
* Executes insertions using a synchronous or asynchronous AWS SDK v2 client.
*
* @param client DynamoDB client
* @param insertions items to insert
* @return per-insertion results
*/
public static DynamoDbInsertionResultsDto executeInsert(Object client, List<DynamoDbInsertionDto> insertions) {
if (client == null) {
throw new IllegalArgumentException("No DynamoDB client");
}
if (insertions == null || insertions.isEmpty()) {
throw new IllegalArgumentException("No data to insert");
}

DynamoDbInsertionResultsDto results = new DynamoDbInsertionResultsDto();
for (int i = 0; i < insertions.size(); i++) {
try {
executeOne(client, insertions.get(i));
} catch (RuntimeException e) {
handleFailedInsertion(results, insertions.size(), i);
throw new DynamoDbInsertionException(i, results, e);
}
}
results.executionResults = new ArrayList<>(Collections.nCopies(insertions.size(), true));
return results;
}

/**
* Records the insertion that failed while preserving earlier successes.
*
* @param results insertion results to update
* @param insertionCount number of attempted insertions
* @param failedIndex zero-based index of the failed insertion
*/
private static void handleFailedInsertion(
DynamoDbInsertionResultsDto results, int insertionCount, int failedIndex) {
results.executionResults = new ArrayList<>(Collections.nCopies(insertionCount, false));
for (int i = 0; i < failedIndex; i++) {
results.executionResults.set(i, true);
}
results.failedInsertionIndex = failedIndex;
}

private static void executeOne(Object client, DynamoDbInsertionDto insertion) {
try {
ClassLoader loader = client.getClass().getClassLoader();
Class<?> attributeValueClass = Class.forName(
"software.amazon.awssdk.services.dynamodb.model.AttributeValue", true, loader);

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.

add constant for these strings

Class<?> attributeValueBuilderClass = Class.forName(
"software.amazon.awssdk.services.dynamodb.model.AttributeValue$Builder", true, loader);
Class<?> putItemRequestClass = Class.forName(
"software.amazon.awssdk.services.dynamodb.model.PutItemRequest", true, loader);
Class<?> putItemRequestBuilderClass = Class.forName(
"software.amazon.awssdk.services.dynamodb.model.PutItemRequest$Builder", true, loader);

Map<String, Object> item = new LinkedHashMap<>();
for (DynamoDbAttributeValueDto attribute : insertion.attributes) {
Object builder = attributeValueClass.getMethod("builder").invoke(null);
String setter;
Object value;
switch (attribute.type) {

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 not having the same d action for all printable values?

case STRING:
setter = "s";
value = attribute.value;
break;
case NUMBER:
setter = "n";
value = attribute.value;
break;
case BOOLEAN:
setter = "bool";
value = Boolean.valueOf(attribute.value);
break;
default:
throw new IllegalArgumentException("Unsupported DynamoDB attribute type: " + attribute.type);
}
attributeValueBuilderClass.getMethod(setter, value.getClass()).invoke(builder, value);
item.put(attribute.attributeName, attributeValueBuilderClass.getMethod("build").invoke(builder));
}

Object requestBuilder = putItemRequestClass.getMethod("builder").invoke(null);

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.

add string constants

putItemRequestBuilderClass.getMethod("tableName", String.class)
.invoke(requestBuilder, insertion.tableName);
putItemRequestBuilderClass.getMethod("item", Map.class).invoke(requestBuilder, item);
Object request = putItemRequestBuilderClass.getMethod("build").invoke(requestBuilder);
Method putItem = findPutItemMethod(client, loader, putItemRequestClass);
Object response = putItem.invoke(client, request);
if (response instanceof CompletionStage) {
((CompletionStage<?>) response).toCompletableFuture().join();
}
} catch (InvocationTargetException e) {
Throwable cause = e.getCause() == null ? e : e.getCause();
throw new RuntimeException("Failed DynamoDB insertion into table '" + insertion.tableName + "'", cause);
} catch (ReflectiveOperationException e) {
throw new RuntimeException("Failed DynamoDB insertion into table '" + insertion.tableName + "'", e);
}
}

private static Method findPutItemMethod(Object client, ClassLoader loader, Class<?> putItemRequestClass)
throws ClassNotFoundException, NoSuchMethodException {
Class<?> syncClientClass = Class.forName("software.amazon.awssdk.services.dynamodb.DynamoDbClient", true, loader);

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.

see comment above

if (syncClientClass.isInstance(client)) {
return syncClientClass.getMethod("putItem", putItemRequestClass);
}

Class<?> asyncClientClass = Class.forName("software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient", true, loader);
if (asyncClientClass.isInstance(client)) {
return asyncClientClass.getMethod("putItem", putItemRequestClass);
}

throw new IllegalArgumentException("Unsupported DynamoDB client: " + client.getClass().getName());
}

/**
* Exception carrying partial insertion results.
*/
public static class DynamoDbInsertionException extends RuntimeException {

private final int failedIndex;
private final DynamoDbInsertionResultsDto results;

private DynamoDbInsertionException(int failedIndex, DynamoDbInsertionResultsDto results, Throwable cause) {
super("Failed DynamoDB insertion at index " + failedIndex, cause);
this.failedIndex = failedIndex;
this.results = results;
}

/**
* @return failed insertion index
*/
public int getFailedIndex() {
return failedIndex;
}

/**
* @return partial results
*/
public DynamoDbInsertionResultsDto getResults() {
return results;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package org.evomaster.client.java.controller.dynamodb;

import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbAttributeValueDto;
import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbInsertionDto;
import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbInsertionResultsDto;
import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbScalarTypeDto;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
import software.amazon.awssdk.services.dynamodb.model.PutItemResponse;

import java.util.Arrays;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/** Tests DynamoDB item insertion through synchronous and asynchronous AWS clients. */
public class DynamoDbCommandExecutorTest {

@Test
public void testExecuteInsertWithSynchronousClient() {
DynamoDbClient client = mock(DynamoDbClient.class);
when(client.putItem(any(PutItemRequest.class))).thenReturn(PutItemResponse.builder().build());

DynamoDbInsertionResultsDto results = DynamoDbCommandExecutor.executeInsert(
client, Collections.singletonList(worldCupPlayer()));

ArgumentCaptor<PutItemRequest> requestCaptor = ArgumentCaptor.forClass(PutItemRequest.class);
verify(client).putItem(requestCaptor.capture());
PutItemRequest request = requestCaptor.getValue();
assertEquals("WorldCupPlayers", request.tableName());
assertEquals("Argentina", request.item().get("country").s());
assertEquals("10", request.item().get("fifaId").n());
assertTrue(request.item().get("captain").bool());
assertEquals(Collections.singletonList(true), results.executionResults);
assertNull(results.failedInsertionIndex);
}

@Test
public void testExecuteInsertWithAsynchronousClient() {
DynamoDbAsyncClient client = mock(DynamoDbAsyncClient.class);
when(client.putItem(any(PutItemRequest.class))).thenReturn(
CompletableFuture.completedFuture(PutItemResponse.builder().build()));

DynamoDbInsertionResultsDto results = DynamoDbCommandExecutor.executeInsert(
client, Collections.singletonList(worldCupPlayer()));

verify(client).putItem(any(PutItemRequest.class));
assertEquals(Collections.singletonList(true), results.executionResults);
}

@Test
public void testFailureContainsPartialResults() {
DynamoDbClient client = mock(DynamoDbClient.class);
when(client.putItem(any(PutItemRequest.class)))
.thenReturn(PutItemResponse.builder().build())
.thenThrow(new IllegalStateException("DynamoDB unavailable"));

DynamoDbCommandExecutor.DynamoDbInsertionException error = assertThrows(
DynamoDbCommandExecutor.DynamoDbInsertionException.class,
() -> DynamoDbCommandExecutor.executeInsert(
client, Arrays.asList(worldCupPlayer(), worldCupPlayer())));

assertEquals(1, error.getFailedIndex());
assertEquals(Arrays.asList(true, false), error.getResults().executionResults);
assertEquals(Integer.valueOf(1), error.getResults().failedInsertionIndex);
verify(client, times(2)).putItem(any(PutItemRequest.class));
}

@Test
public void testRejectsMissingClientOrInsertions() {
DynamoDbClient client = mock(DynamoDbClient.class);

assertThrows(IllegalArgumentException.class,
() -> DynamoDbCommandExecutor.executeInsert(null, Collections.singletonList(worldCupPlayer())));
assertThrows(IllegalArgumentException.class,
() -> DynamoDbCommandExecutor.executeInsert(client, null));
assertThrows(IllegalArgumentException.class,
() -> DynamoDbCommandExecutor.executeInsert(client, Collections.emptyList()));
verify(client, times(0)).putItem(any(PutItemRequest.class));
}

private DynamoDbInsertionDto worldCupPlayer() {
DynamoDbInsertionDto insertion = new DynamoDbInsertionDto();
insertion.tableName = "WorldCupPlayers";
insertion.attributes.add(new DynamoDbAttributeValueDto("country", DynamoDbScalarTypeDto.STRING, "Argentina"));
insertion.attributes.add(new DynamoDbAttributeValueDto("fifaId", DynamoDbScalarTypeDto.NUMBER, "10"));
insertion.attributes.add(new DynamoDbAttributeValueDto("captain", DynamoDbScalarTypeDto.BOOLEAN, "true"));
return insertion;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package org.evomaster.core.database.dynamodb

import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbScalarTypeDto
import org.evomaster.core.search.action.Action
import org.evomaster.core.search.action.EnvironmentAction
import org.evomaster.core.search.gene.Gene

/**
* A typed attribute gene belonging to a DynamoDB item.
*
* @property attributeName name of the DynamoDB item attribute
* @property type supported DynamoDB scalar type
* @property gene evolvable value for the attribute
*/
data class DynamoDbAttributeGene(
val attributeName: String,
val type: DynamoDbScalarTypeDto,
val gene: Gene
)

/**
* An initialization action that inserts one DynamoDB item.
*
* @property tableName target DynamoDB table
* @property attributes item attributes to insert
*/
class DynamoDbAction(
val tableName: String,
val attributes: List<DynamoDbAttributeGene>
) : EnvironmentAction(listOf()) {

init {
addChildren(attributes.map { it.gene })
}

/** Returns the genes that determine the inserted item values. */
override fun seeTopGenes(): List<Gene> = attributes.map { it.gene }

/** Creates an independent action with copies of all attribute genes. */
override fun copyContent(): Action = DynamoDbAction(
tableName,
attributes.map { DynamoDbAttributeGene(it.attributeName, it.type, it.gene.copy()) }
)

/** Returns the descriptive name of this insertion action. */
override fun getName(): String = "DynamoDB_INSERT_$tableName"

/** Returns the grouping key for DynamoDB initialization actions. */
override fun getActionGroupKey(): String = DynamoDbAction::class.java.name

/** Stable key used to avoid adding the same inferred insertion twice. */
fun insertionKey(): String = buildString {
append(tableName)
attributes.forEach {
append('|').append(it.attributeName).append(':').append(it.type)

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.

add constants

.append('=').append(it.gene.getValueAsRawString())
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.evomaster.core.database.dynamodb

import org.evomaster.core.search.action.Action
import org.evomaster.core.search.action.ActionResult

/** Result of executing a [DynamoDbAction]. */
class DynamoDbActionResult : ActionResult {

/** Creates a result for the action identified by [sourceLocalId]. */
constructor(sourceLocalId: String, stopping: Boolean = false) : super(sourceLocalId, stopping)

/** Creates a copy of another DynamoDB action result. */
constructor(other: DynamoDbActionResult) : super(other)

companion object {
const val INSERT_DYNAMODB_EXECUTE_SUCCESSFULLY = "INSERT_DYNAMODB_EXECUTE_SUCCESSFULLY"
}

/** Creates an independent copy of this result. */
override fun copy(): DynamoDbActionResult = DynamoDbActionResult(this)

/** Records whether the insertion completed successfully. */
fun setInsertExecutionResult(success: Boolean) =
addResultValue(INSERT_DYNAMODB_EXECUTE_SUCCESSFULLY, success.toString())

/** Returns whether the insertion completed successfully. */
fun getInsertExecutionResult(): Boolean =
getResultValue(INSERT_DYNAMODB_EXECUTE_SUCCESSFULLY)?.toBoolean() ?: false

/** Returns whether [action] is a DynamoDB insertion action. */
override fun matchedType(action: Action): Boolean = action is DynamoDbAction
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.evomaster.core.database.dynamodb

import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbAttributeValueDto
import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbDatabaseCommandsDto
import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbInsertionDto
import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbScalarTypeDto
import org.evomaster.core.search.gene.BooleanGene
import org.evomaster.core.search.gene.numeric.BigDecimalGene
import org.evomaster.core.search.gene.string.StringGene

/** Transforms DynamoDB actions into controller insertion commands. */
object DynamoDbActionTransformer {

/** Converts initialization actions to the controller's DynamoDB insertion DTO. */
fun transform(actions: List<DynamoDbAction>): DynamoDbDatabaseCommandsDto =
DynamoDbDatabaseCommandsDto().also { commands ->
commands.insertions = actions.map { action ->
DynamoDbInsertionDto().also { insertion ->
insertion.tableName = action.tableName
insertion.attributes = action.attributes.map { attribute ->
DynamoDbAttributeValueDto(
attribute.attributeName,
attribute.type,
when (attribute.type) {
DynamoDbScalarTypeDto.STRING -> (attribute.gene as StringGene).value
DynamoDbScalarTypeDto.NUMBER -> (attribute.gene as BigDecimalGene).value.toPlainString()
DynamoDbScalarTypeDto.BOOLEAN -> (attribute.gene as BooleanGene).value.toString()
}
)
}
}
}
}
}
Loading
Loading