From b6fbb523d8a4a41ace235fbae4854a104aeb07b3 Mon Sep 17 00:00:00 2001 From: aschenzle Date: Sun, 30 Aug 2026 11:04:48 -0700 Subject: [PATCH 1/2] DynamoDB insertions core actions, executor, builder. --- .../dynamodb/DynamoDbCommandExecutor.java | 151 ++++++++++++++++++ .../dynamodb/DynamoDbCommandExecutorTest.java | 99 ++++++++++++ .../core/database/dynamodb/DynamoDbAction.kt | 59 +++++++ .../database/dynamodb/DynamoDbActionResult.kt | 32 ++++ .../dynamodb/DynamoDbActionTransformer.kt | 34 ++++ .../database/dynamodb/DynamoDbExecution.kt | 15 ++ .../dynamodb/DynamoDbInsertBuilder.kt | 55 +++++++ .../database/dynamodb/DynamoDbActionTest.kt | 65 ++++++++ .../dynamodb/DynamoDbActionTransformerTest.kt | 42 +++++ .../dynamodb/DynamoDbInsertBuilderTest.kt | 63 ++++++++ 10 files changed, 615 insertions(+) create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbCommandExecutor.java create mode 100644 client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbCommandExecutorTest.java create mode 100644 core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbAction.kt create mode 100644 core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionResult.kt create mode 100644 core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTransformer.kt create mode 100644 core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbExecution.kt create mode 100644 core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbInsertBuilder.kt create mode 100644 core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTest.kt create mode 100644 core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTransformerTest.kt create mode 100644 core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbInsertBuilderTest.kt diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbCommandExecutor.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbCommandExecutor.java new file mode 100644 index 0000000000..87aca45252 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbCommandExecutor.java @@ -0,0 +1,151 @@ +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 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(); + results.executionResults = new ArrayList<>(Collections.nCopies(insertions.size(), false)); + for (int i = 0; i < insertions.size(); i++) { + try { + executeOne(client, insertions.get(i)); + results.executionResults.set(i, true); + } catch (RuntimeException e) { + results.failedInsertionIndex = i; + throw new DynamoDbInsertionException(i, results, e); + } + } + return results; + } + + 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); + 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 item = new LinkedHashMap<>(); + for (DynamoDbAttributeValueDto attribute : insertion.attributes) { + Object builder = attributeValueClass.getMethod("builder").invoke(null); + String setter; + Object value; + switch (attribute.type) { + case S: + setter = "s"; + value = attribute.value; + break; + case N: + setter = "n"; + value = attribute.value; + break; + case BOOL: + 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); + 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); + 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; + } + } +} diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbCommandExecutorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbCommandExecutorTest.java new file mode 100644 index 0000000000..800091d406 --- /dev/null +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbCommandExecutorTest.java @@ -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 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.S, "Argentina")); + insertion.attributes.add(new DynamoDbAttributeValueDto("fifaId", DynamoDbScalarTypeDto.N, "10")); + insertion.attributes.add(new DynamoDbAttributeValueDto("captain", DynamoDbScalarTypeDto.BOOL, "true")); + return insertion; + } +} diff --git a/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbAction.kt b/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbAction.kt new file mode 100644 index 0000000000..8dc307396b --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbAction.kt @@ -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 +) : EnvironmentAction(listOf()) { + + init { + addChildren(attributes.map { it.gene }) + } + + /** Returns the genes that determine the inserted item values. */ + override fun seeTopGenes(): List = 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) + .append('=').append(it.gene.getValueAsRawString()) + } + } +} diff --git a/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionResult.kt b/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionResult.kt new file mode 100644 index 0000000000..48634f2d89 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionResult.kt @@ -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 +} diff --git a/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTransformer.kt b/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTransformer.kt new file mode 100644 index 0000000000..c01a6f81a6 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTransformer.kt @@ -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): 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.S -> (attribute.gene as StringGene).value + DynamoDbScalarTypeDto.N -> (attribute.gene as BigDecimalGene).value.toPlainString() + DynamoDbScalarTypeDto.BOOL -> (attribute.gene as BooleanGene).value.toString() + } + ) + } + } + } + } +} diff --git a/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbExecution.kt b/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbExecution.kt new file mode 100644 index 0000000000..8c684d54ac --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbExecution.kt @@ -0,0 +1,15 @@ +package org.evomaster.core.database.dynamodb + +import org.evomaster.client.java.controller.api.dto.database.execution.DynamoDbExecutionsDto +import org.evomaster.client.java.controller.api.dto.database.execution.DynamoDbFailedQuery + +/** Failed DynamoDB reads observed during one action. */ +class DynamoDbExecution(val failedQueries: List) { + + companion object { + + /** Creates an execution view from the controller response, handling a missing response. */ + fun fromDto(dto: DynamoDbExecutionsDto?): DynamoDbExecution = + DynamoDbExecution(dto?.failedQueries ?: emptyList()) + } +} diff --git a/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbInsertBuilder.kt b/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbInsertBuilder.kt new file mode 100644 index 0000000000..1f61f61ca6 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbInsertBuilder.kt @@ -0,0 +1,55 @@ +package org.evomaster.core.database.dynamodb + +import org.evomaster.client.java.controller.api.dto.database.execution.DynamoDbFailedQuery +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbAttributeValueDto +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 + +/** Builds evolvable DynamoDB insertion actions from failed equality reads. */ +object DynamoDbInsertBuilder { + + /** + * Builds unique, supported DynamoDB insertion actions from failed equality reads. + * + * @param failedQueries failed reads reported by the controller + * @param existingInsertionKeys keys of insertions that have already been added to the individual + * @return inferred actions not already represented by [existingInsertionKeys] + */ + fun buildInsertActions( + failedQueries: List, + existingInsertionKeys: Set + ): List = failedQueries + .mapNotNull(::toActionOrNull) + .filterNot { it.insertionKey() in existingInsertionKeys } + .distinctBy { it.insertionKey() } + + /** Converts one failed read into an insertion action, or returns null when it is incomplete. */ + private fun toActionOrNull(query: DynamoDbFailedQuery): DynamoDbAction? { + val tableName = query.tableName + val queryAttributes = query.attributes + if (tableName.isNullOrBlank() || queryAttributes.isNullOrEmpty()) return null + + val attributes = queryAttributes.map { attribute -> + toAttributeOrNull(attribute) ?: return null + } + + return DynamoDbAction(tableName, attributes) + } + + /** Converts a supported scalar DynamoDB attribute into its evolvable representation. */ + private fun toAttributeOrNull(attribute: DynamoDbAttributeValueDto): DynamoDbAttributeGene? { + val type = attribute.type ?: return null + val value = attribute.value ?: return null + val gene = when (type) { + DynamoDbScalarTypeDto.S -> StringGene(attribute.attributeName, value) + DynamoDbScalarTypeDto.N -> value.toBigDecimalOrNull()?.let { + BigDecimalGene(attribute.attributeName, it) + } ?: return null + DynamoDbScalarTypeDto.BOOL -> BooleanGene(attribute.attributeName, value.toBoolean()) + } + + return DynamoDbAttributeGene(attribute.attributeName, type, gene) + } +} diff --git a/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTest.kt b/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTest.kt new file mode 100644 index 0000000000..a5e40694b8 --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTest.kt @@ -0,0 +1,65 @@ +package org.evomaster.core.database.dynamodb + +import org.evomaster.client.java.controller.api.dto.database.execution.DynamoDbExecutionsDto +import org.evomaster.client.java.controller.api.dto.database.execution.DynamoDbFailedQuery +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbAttributeValueDto +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbScalarTypeDto +import org.evomaster.core.search.gene.string.StringGene +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotSame +import org.junit.jupiter.api.Assertions.assertSame +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +/** Tests the DynamoDB initialization action and its execution result. */ +class DynamoDbActionTest { + + @Test + fun actionExposesStableMetadataAndCopiesGenesIndependently() { + val original = DynamoDbAction( + "WorldCupPlayers", + listOf(DynamoDbAttributeGene("country", DynamoDbScalarTypeDto.S, StringGene("country", "Argentina"))) + ) + + val copy = original.copy() as DynamoDbAction + (copy.attributes.single().gene as StringGene).value = "Brazil" + + assertEquals("DynamoDB_INSERT_WorldCupPlayers", original.getName()) + assertEquals(DynamoDbAction::class.java.name, original.getActionGroupKey()) + assertEquals("WorldCupPlayers|country:S=Argentina", original.insertionKey()) + assertSame(original.attributes.single().gene, original.seeTopGenes().single()) + assertNotSame(original.attributes.single().gene, copy.attributes.single().gene) + assertEquals("Argentina", (original.attributes.single().gene as StringGene).value) + assertEquals("Brazil", (copy.attributes.single().gene as StringGene).value) + } + + @Test + fun actionResultTracksInsertionOutcomeAndMatchesDynamoDbActions() { + val action = DynamoDbAction( + "WorldCupPlayers", + listOf(DynamoDbAttributeGene("country", DynamoDbScalarTypeDto.S, StringGene("country", "Argentina"))) + ) + val result = DynamoDbActionResult("source") + + assertFalse(result.getInsertExecutionResult()) + result.setInsertExecutionResult(true) + + assertTrue(result.getInsertExecutionResult()) + assertTrue(result.matchedType(action)) + assertTrue(result.copy().getInsertExecutionResult()) + } + + @Test + fun executionPreservesFailedQueriesAndAcceptsMissingDto() { + val query = DynamoDbFailedQuery( + "WorldCupPlayers", + listOf(DynamoDbAttributeValueDto("country", DynamoDbScalarTypeDto.S, "Argentina")) + ) + val dto = DynamoDbExecutionsDto() + dto.failedQueries.add(query) + + assertSame(query, DynamoDbExecution.fromDto(dto).failedQueries.single()) + assertTrue(DynamoDbExecution.fromDto(null).failedQueries.isEmpty()) + } +} diff --git a/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTransformerTest.kt b/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTransformerTest.kt new file mode 100644 index 0000000000..53137d3c1f --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTransformerTest.kt @@ -0,0 +1,42 @@ +package org.evomaster.core.database.dynamodb + +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 +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import java.math.BigDecimal + +/** Tests conversion of DynamoDB initialization actions to controller DTOs. */ +class DynamoDbActionTransformerTest { + + @Test + fun transformsAllSupportedScalarTypes() { + val action = DynamoDbAction( + "WorldCupPlayers", + listOf( + DynamoDbAttributeGene("country", DynamoDbScalarTypeDto.S, StringGene("country", "Argentina")), + DynamoDbAttributeGene("fifaId", DynamoDbScalarTypeDto.N, BigDecimalGene("fifaId", BigDecimal("10.50"))), + DynamoDbAttributeGene("captain", DynamoDbScalarTypeDto.BOOL, BooleanGene("captain", true)) + ) + ) + + val insertion = DynamoDbActionTransformer.transform(listOf(action)).insertions.single() + + assertEquals("WorldCupPlayers", insertion.tableName) + assertEquals("Argentina", insertion.attributes[0].value) + assertEquals("10.50", insertion.attributes[1].value) + assertEquals("true", insertion.attributes[2].value) + assertEquals( + listOf(DynamoDbScalarTypeDto.S, DynamoDbScalarTypeDto.N, DynamoDbScalarTypeDto.BOOL), + insertion.attributes.map { it.type } + ) + } + + @Test + fun transformsAnEmptyActionList() { + assertTrue(DynamoDbActionTransformer.transform(emptyList()).insertions.isEmpty()) + } +} diff --git a/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbInsertBuilderTest.kt b/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbInsertBuilderTest.kt new file mode 100644 index 0000000000..d936f84651 --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbInsertBuilderTest.kt @@ -0,0 +1,63 @@ +package org.evomaster.core.database.dynamodb + +import org.evomaster.client.java.controller.api.dto.database.execution.DynamoDbFailedQuery +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbAttributeValueDto +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 +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +/** Tests inference of DynamoDB initialization actions from failed reads. */ +class DynamoDbInsertBuilderTest { + + @Test + fun buildsTypedActionFromSupportedFailedQuery() { + val actions = DynamoDbInsertBuilder.buildInsertActions(listOf(validQuery()), emptySet()) + + assertEquals(1, actions.size) + val attributes = actions.single().attributes + assertEquals("WorldCupPlayers", actions.single().tableName) + assertEquals("Argentina", (attributes[0].gene as StringGene).value) + assertEquals("10.50", (attributes[1].gene as BigDecimalGene).value.toPlainString()) + assertTrue((attributes[2].gene as BooleanGene).value) + } + + @Test + fun skipsInvalidQueriesAndRemovesDuplicateActions() { + val valid = validQuery() + val invalidNumber = DynamoDbFailedQuery( + "WorldCupPlayers", + listOf(DynamoDbAttributeValueDto("fifaId", DynamoDbScalarTypeDto.N, "ten")) + ) + val blankTable = DynamoDbFailedQuery( + "", + listOf(DynamoDbAttributeValueDto("country", DynamoDbScalarTypeDto.S, "Argentina")) + ) + val missingType = DynamoDbFailedQuery( + "WorldCupPlayers", + listOf(DynamoDbAttributeValueDto("country", null, "Argentina")) + ) + + val actions = DynamoDbInsertBuilder.buildInsertActions( + listOf(valid, valid, invalidNumber, blankTable, missingType), + emptySet() + ) + + assertEquals(1, actions.size) + assertTrue( + DynamoDbInsertBuilder.buildInsertActions(listOf(valid), setOf(actions.single().insertionKey())).isEmpty() + ) + } + + private fun validQuery(): DynamoDbFailedQuery = DynamoDbFailedQuery( + "WorldCupPlayers", + listOf( + DynamoDbAttributeValueDto("country", DynamoDbScalarTypeDto.S, "Argentina"), + DynamoDbAttributeValueDto("fifaId", DynamoDbScalarTypeDto.N, "10.50"), + DynamoDbAttributeValueDto("captain", DynamoDbScalarTypeDto.BOOL, "true") + ) + ) +} From 2d038355b85f085f3d0c26b251d4dcac06f5bc69 Mon Sep 17 00:00:00 2001 From: aschenzle Date: Fri, 4 Sep 2026 22:28:12 -0700 Subject: [PATCH 2/2] Updating to latest version of DTOs, added the removed logic here --- .../dynamodb/DynamoDbCommandExecutor.java | 27 ++++++++++++++----- .../dynamodb/DynamoDbCommandExecutorTest.java | 6 ++--- .../dynamodb/DynamoDbActionTransformer.kt | 6 ++--- .../dynamodb/DynamoDbInsertBuilder.kt | 6 ++--- .../database/dynamodb/DynamoDbActionTest.kt | 8 +++--- .../dynamodb/DynamoDbActionTransformerTest.kt | 8 +++--- .../dynamodb/DynamoDbInsertBuilderTest.kt | 10 +++---- 7 files changed, 43 insertions(+), 28 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbCommandExecutor.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbCommandExecutor.java index 87aca45252..4742a44ec1 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbCommandExecutor.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbCommandExecutor.java @@ -37,19 +37,34 @@ public static DynamoDbInsertionResultsDto executeInsert(Object client, List(Collections.nCopies(insertions.size(), false)); for (int i = 0; i < insertions.size(); i++) { try { executeOne(client, insertions.get(i)); - results.executionResults.set(i, true); } catch (RuntimeException e) { - results.failedInsertionIndex = i; + 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(); @@ -68,15 +83,15 @@ private static void executeOne(Object client, DynamoDbInsertionDto insertion) { String setter; Object value; switch (attribute.type) { - case S: + case STRING: setter = "s"; value = attribute.value; break; - case N: + case NUMBER: setter = "n"; value = attribute.value; break; - case BOOL: + case BOOLEAN: setter = "bool"; value = Boolean.valueOf(attribute.value); break; diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbCommandExecutorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbCommandExecutorTest.java index 800091d406..838da8b108 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbCommandExecutorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbCommandExecutorTest.java @@ -91,9 +91,9 @@ public void testRejectsMissingClientOrInsertions() { private DynamoDbInsertionDto worldCupPlayer() { DynamoDbInsertionDto insertion = new DynamoDbInsertionDto(); insertion.tableName = "WorldCupPlayers"; - insertion.attributes.add(new DynamoDbAttributeValueDto("country", DynamoDbScalarTypeDto.S, "Argentina")); - insertion.attributes.add(new DynamoDbAttributeValueDto("fifaId", DynamoDbScalarTypeDto.N, "10")); - insertion.attributes.add(new DynamoDbAttributeValueDto("captain", DynamoDbScalarTypeDto.BOOL, "true")); + 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; } } diff --git a/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTransformer.kt b/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTransformer.kt index c01a6f81a6..b379cebb7b 100644 --- a/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTransformer.kt +++ b/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTransformer.kt @@ -22,9 +22,9 @@ object DynamoDbActionTransformer { attribute.attributeName, attribute.type, when (attribute.type) { - DynamoDbScalarTypeDto.S -> (attribute.gene as StringGene).value - DynamoDbScalarTypeDto.N -> (attribute.gene as BigDecimalGene).value.toPlainString() - DynamoDbScalarTypeDto.BOOL -> (attribute.gene as BooleanGene).value.toString() + DynamoDbScalarTypeDto.STRING -> (attribute.gene as StringGene).value + DynamoDbScalarTypeDto.NUMBER -> (attribute.gene as BigDecimalGene).value.toPlainString() + DynamoDbScalarTypeDto.BOOLEAN -> (attribute.gene as BooleanGene).value.toString() } ) } diff --git a/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbInsertBuilder.kt b/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbInsertBuilder.kt index 1f61f61ca6..5852bc82ac 100644 --- a/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbInsertBuilder.kt +++ b/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbInsertBuilder.kt @@ -43,11 +43,11 @@ object DynamoDbInsertBuilder { val type = attribute.type ?: return null val value = attribute.value ?: return null val gene = when (type) { - DynamoDbScalarTypeDto.S -> StringGene(attribute.attributeName, value) - DynamoDbScalarTypeDto.N -> value.toBigDecimalOrNull()?.let { + DynamoDbScalarTypeDto.STRING -> StringGene(attribute.attributeName, value) + DynamoDbScalarTypeDto.NUMBER -> value.toBigDecimalOrNull()?.let { BigDecimalGene(attribute.attributeName, it) } ?: return null - DynamoDbScalarTypeDto.BOOL -> BooleanGene(attribute.attributeName, value.toBoolean()) + DynamoDbScalarTypeDto.BOOLEAN -> BooleanGene(attribute.attributeName, value.toBoolean()) } return DynamoDbAttributeGene(attribute.attributeName, type, gene) diff --git a/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTest.kt b/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTest.kt index a5e40694b8..609a631ebd 100644 --- a/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTest.kt @@ -19,7 +19,7 @@ class DynamoDbActionTest { fun actionExposesStableMetadataAndCopiesGenesIndependently() { val original = DynamoDbAction( "WorldCupPlayers", - listOf(DynamoDbAttributeGene("country", DynamoDbScalarTypeDto.S, StringGene("country", "Argentina"))) + listOf(DynamoDbAttributeGene("country", DynamoDbScalarTypeDto.STRING, StringGene("country", "Argentina"))) ) val copy = original.copy() as DynamoDbAction @@ -27,7 +27,7 @@ class DynamoDbActionTest { assertEquals("DynamoDB_INSERT_WorldCupPlayers", original.getName()) assertEquals(DynamoDbAction::class.java.name, original.getActionGroupKey()) - assertEquals("WorldCupPlayers|country:S=Argentina", original.insertionKey()) + assertEquals("WorldCupPlayers|country:STRING=Argentina", original.insertionKey()) assertSame(original.attributes.single().gene, original.seeTopGenes().single()) assertNotSame(original.attributes.single().gene, copy.attributes.single().gene) assertEquals("Argentina", (original.attributes.single().gene as StringGene).value) @@ -38,7 +38,7 @@ class DynamoDbActionTest { fun actionResultTracksInsertionOutcomeAndMatchesDynamoDbActions() { val action = DynamoDbAction( "WorldCupPlayers", - listOf(DynamoDbAttributeGene("country", DynamoDbScalarTypeDto.S, StringGene("country", "Argentina"))) + listOf(DynamoDbAttributeGene("country", DynamoDbScalarTypeDto.STRING, StringGene("country", "Argentina"))) ) val result = DynamoDbActionResult("source") @@ -54,7 +54,7 @@ class DynamoDbActionTest { fun executionPreservesFailedQueriesAndAcceptsMissingDto() { val query = DynamoDbFailedQuery( "WorldCupPlayers", - listOf(DynamoDbAttributeValueDto("country", DynamoDbScalarTypeDto.S, "Argentina")) + listOf(DynamoDbAttributeValueDto("country", DynamoDbScalarTypeDto.STRING, "Argentina")) ) val dto = DynamoDbExecutionsDto() dto.failedQueries.add(query) diff --git a/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTransformerTest.kt b/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTransformerTest.kt index 53137d3c1f..032e50fca5 100644 --- a/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTransformerTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTransformerTest.kt @@ -17,9 +17,9 @@ class DynamoDbActionTransformerTest { val action = DynamoDbAction( "WorldCupPlayers", listOf( - DynamoDbAttributeGene("country", DynamoDbScalarTypeDto.S, StringGene("country", "Argentina")), - DynamoDbAttributeGene("fifaId", DynamoDbScalarTypeDto.N, BigDecimalGene("fifaId", BigDecimal("10.50"))), - DynamoDbAttributeGene("captain", DynamoDbScalarTypeDto.BOOL, BooleanGene("captain", true)) + DynamoDbAttributeGene("country", DynamoDbScalarTypeDto.STRING, StringGene("country", "Argentina")), + DynamoDbAttributeGene("fifaId", DynamoDbScalarTypeDto.NUMBER, BigDecimalGene("fifaId", BigDecimal("10.50"))), + DynamoDbAttributeGene("captain", DynamoDbScalarTypeDto.BOOLEAN, BooleanGene("captain", true)) ) ) @@ -30,7 +30,7 @@ class DynamoDbActionTransformerTest { assertEquals("10.50", insertion.attributes[1].value) assertEquals("true", insertion.attributes[2].value) assertEquals( - listOf(DynamoDbScalarTypeDto.S, DynamoDbScalarTypeDto.N, DynamoDbScalarTypeDto.BOOL), + listOf(DynamoDbScalarTypeDto.STRING, DynamoDbScalarTypeDto.NUMBER, DynamoDbScalarTypeDto.BOOLEAN), insertion.attributes.map { it.type } ) } diff --git a/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbInsertBuilderTest.kt b/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbInsertBuilderTest.kt index d936f84651..ef512ab285 100644 --- a/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbInsertBuilderTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbInsertBuilderTest.kt @@ -30,11 +30,11 @@ class DynamoDbInsertBuilderTest { val valid = validQuery() val invalidNumber = DynamoDbFailedQuery( "WorldCupPlayers", - listOf(DynamoDbAttributeValueDto("fifaId", DynamoDbScalarTypeDto.N, "ten")) + listOf(DynamoDbAttributeValueDto("fifaId", DynamoDbScalarTypeDto.NUMBER, "ten")) ) val blankTable = DynamoDbFailedQuery( "", - listOf(DynamoDbAttributeValueDto("country", DynamoDbScalarTypeDto.S, "Argentina")) + listOf(DynamoDbAttributeValueDto("country", DynamoDbScalarTypeDto.STRING, "Argentina")) ) val missingType = DynamoDbFailedQuery( "WorldCupPlayers", @@ -55,9 +55,9 @@ class DynamoDbInsertBuilderTest { private fun validQuery(): DynamoDbFailedQuery = DynamoDbFailedQuery( "WorldCupPlayers", listOf( - DynamoDbAttributeValueDto("country", DynamoDbScalarTypeDto.S, "Argentina"), - DynamoDbAttributeValueDto("fifaId", DynamoDbScalarTypeDto.N, "10.50"), - DynamoDbAttributeValueDto("captain", DynamoDbScalarTypeDto.BOOL, "true") + DynamoDbAttributeValueDto("country", DynamoDbScalarTypeDto.STRING, "Argentina"), + DynamoDbAttributeValueDto("fifaId", DynamoDbScalarTypeDto.NUMBER, "10.50"), + DynamoDbAttributeValueDto("captain", DynamoDbScalarTypeDto.BOOLEAN, "true") ) ) }