From 91a37206439e9ccd2384b5c6ead40af02698e2ac Mon Sep 17 00:00:00 2001 From: aschenzle Date: Thu, 10 Sep 2026 22:42:54 -0700 Subject: [PATCH] DynamoDB insertion integration plus e2e test --- .../WorldCupPlayersEmptyController.java | 39 ++++++++++++ .../dynamodb/DynamoDbInsertionsEMTest.java | 45 +++++++++++++ .../rest/dynamodb/DynamoDbTestBase.java | 15 +++++ .../kotlin/org/evomaster/core/EMConfig.kt | 22 +++++++ .../api/service/ApiWsStructureMutator.kt | 63 +++++++++++++++++++ .../enterprise/service/EnterpriseFitness.kt | 25 ++++++++ .../service/fitness/ResourceRestFitness.kt | 3 + .../rest/service/fitness/RestFitness.kt | 2 + .../core/search/EvaluatedIndividual.kt | 3 +- .../org/evomaster/core/search/FitnessValue.kt | 19 ++++++ .../ImpactsOfIndividual.kt | 3 + .../kotlin/org/evomaster/core/TestUtils.kt | 17 +++++ .../rest/individual/RestIndividualTest.kt | 16 ++++- docs/options.md | 2 + 14 files changed, 270 insertions(+), 4 deletions(-) create mode 100644 core-tests/e2e-tests/spring/spring-rest-dynamodb/src/test/java/com/foo/spring/rest/dynamodb/WorldCupPlayersEmptyController.java create mode 100644 core-tests/e2e-tests/spring/spring-rest-dynamodb/src/test/java/org/evomaster/e2etests/spring/rest/dynamodb/DynamoDbInsertionsEMTest.java diff --git a/core-tests/e2e-tests/spring/spring-rest-dynamodb/src/test/java/com/foo/spring/rest/dynamodb/WorldCupPlayersEmptyController.java b/core-tests/e2e-tests/spring/spring-rest-dynamodb/src/test/java/com/foo/spring/rest/dynamodb/WorldCupPlayersEmptyController.java new file mode 100644 index 0000000000..c4537d1783 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-dynamodb/src/test/java/com/foo/spring/rest/dynamodb/WorldCupPlayersEmptyController.java @@ -0,0 +1,39 @@ +package com.foo.spring.rest.dynamodb; + +import com.dynamodb.players.WorldCupPlayersApp; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition; +import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; +import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement; +import software.amazon.awssdk.services.dynamodb.model.KeyType; +import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; + +/** Embedded controller for an empty World Cup players DynamoDB table. */ +public class WorldCupPlayersEmptyController extends DynamoDbController { + + /** Creates the table used by the read-only player endpoint without seed items. */ + @Override + protected void initializeDatabase(DynamoDbAsyncClient client) { + client.createTable(CreateTableRequest.builder() + .tableName("WorldCupPlayers") + .attributeDefinitions(AttributeDefinition.builder() + .attributeName("country") + .attributeType(ScalarAttributeType.S) + .build()) + .keySchema(KeySchemaElement.builder().attributeName("country").keyType(KeyType.HASH).build()) + .provisionedThroughput(ProvisionedThroughput.builder() + .readCapacityUnits(1L).writeCapacityUnits(1L).build()) + .build()).join(); + } + + @Override + protected Class getApplicationClass() { + return WorldCupPlayersApp.class; + } + + @Override + public String getPackagePrefixesToCover() { + return "com.dynamodb.players"; + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-dynamodb/src/test/java/org/evomaster/e2etests/spring/rest/dynamodb/DynamoDbInsertionsEMTest.java b/core-tests/e2e-tests/spring/spring-rest-dynamodb/src/test/java/org/evomaster/e2etests/spring/rest/dynamodb/DynamoDbInsertionsEMTest.java new file mode 100644 index 0000000000..47ee51f512 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-dynamodb/src/test/java/org/evomaster/e2etests/spring/rest/dynamodb/DynamoDbInsertionsEMTest.java @@ -0,0 +1,45 @@ +package org.evomaster.e2etests.spring.rest.dynamodb; + +import com.foo.spring.rest.dynamodb.WorldCupPlayersEmptyController; +import org.evomaster.core.problem.rest.data.HttpVerb; +import org.evomaster.core.problem.rest.data.RestIndividual; +import org.evomaster.core.search.Solution; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +/** Verifies that a generated DynamoDB insertion satisfies a failed conditional player read. */ +public class DynamoDbInsertionsEMTest extends DynamoDbTestBase { + + /** Starts the empty-table application. */ + @BeforeAll + public static void initClass() throws Exception { + initDynamoDbTest(new WorldCupPlayersEmptyController()); + } + + /** Compares the empty-table baseline with insertion-enabled DynamoDB generation. */ + @Test + public void testInsertionSatisfiesConditionalQuery() throws Throwable { + runTestHandlingFlakyAndCompilation( + "DynamoDbInsertionsEM", + "org.foo.spring.rest.dynamodb.DynamoDbInsertionsEM", + 1000, + false, + args -> { + List baselineArgs = new ArrayList<>(args); + configureDynamoDbInsertions(baselineArgs, false); + Solution baseline = initAndRun(baselineArgs); + assertHasAtLeastOne(baseline, HttpVerb.GET, 404, "/players/{fifaId}", null); + assertNone(baseline, HttpVerb.GET, 200, "/players/{fifaId}", null); + + List insertionArgs = new ArrayList<>(args); + configureDynamoDbInsertions(insertionArgs, true); + Solution generated = initAndRun(insertionArgs); + assertHasAtLeastOne(generated, HttpVerb.GET, 200, "/players/{fifaId}", + "Lionel Messi plays for Argentina"); + }, + 5); + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-dynamodb/src/test/java/org/evomaster/e2etests/spring/rest/dynamodb/DynamoDbTestBase.java b/core-tests/e2e-tests/spring/spring-rest-dynamodb/src/test/java/org/evomaster/e2etests/spring/rest/dynamodb/DynamoDbTestBase.java index 587c388d76..933c413e27 100644 --- a/core-tests/e2e-tests/spring/spring-rest-dynamodb/src/test/java/org/evomaster/e2etests/spring/rest/dynamodb/DynamoDbTestBase.java +++ b/core-tests/e2e-tests/spring/spring-rest-dynamodb/src/test/java/org/evomaster/e2etests/spring/rest/dynamodb/DynamoDbTestBase.java @@ -13,6 +13,8 @@ public abstract class DynamoDbTestBase extends RestTestBase { private static final String INSTRUMENT_DYNAMODB_OPTION = "instrumentMR_DYNAMODB"; private static final String DYNAMODB_HEURISTICS_OPTION = "heuristicsForDynamoDb"; + private static final String DYNAMODB_EXTRACTION_OPTION = "extractDynamoDbExecutionInfo"; + private static final String DYNAMODB_GENERATION_OPTION = "generateDynamoDbData"; /** * Starts an instrumented DynamoDB SUT for a concrete test class. @@ -36,4 +38,17 @@ protected void configureDynamoDbHeuristics(List args, boolean heuristics setOption(args, INSTRUMENT_DYNAMODB_OPTION, "true"); setOption(args, DYNAMODB_HEURISTICS_OPTION, Boolean.toString(heuristicsEnabled)); } + + /** + * Configures DynamoDB failed-read extraction and generated initialization data. + * + * @param args EvoMaster command-line arguments + * @param enabled whether insertion generation should be enabled + */ + protected void configureDynamoDbInsertions(List args, boolean enabled) { + setOption(args, INSTRUMENT_DYNAMODB_OPTION, "true"); + setOption(args, DYNAMODB_HEURISTICS_OPTION, Boolean.toString(enabled)); + setOption(args, DYNAMODB_EXTRACTION_OPTION, Boolean.toString(enabled)); + setOption(args, DYNAMODB_GENERATION_OPTION, Boolean.toString(enabled)); + } } diff --git a/core/src/main/kotlin/org/evomaster/core/EMConfig.kt b/core/src/main/kotlin/org/evomaster/core/EMConfig.kt index 39ad6f5281..ba4519ed6a 100644 --- a/core/src/main/kotlin/org/evomaster/core/EMConfig.kt +++ b/core/src/main/kotlin/org/evomaster/core/EMConfig.kt @@ -780,6 +780,16 @@ class EMConfig { "extracting Mongo execution info with 'extractMongoExecutionInfo'") } + if (shouldGenerateDynamoDbData() && !heuristicsForDynamoDb) { + throw ConfigProblemException("Cannot generate DynamoDB data without enabling " + + "'heuristicsForDynamoDb'") + } + + if (shouldGenerateDynamoDbData() && !extractDynamoDbExecutionInfo) { + throw ConfigProblemException("Cannot generate DynamoDB data without enabling " + + "'extractDynamoDbExecutionInfo'") + } + if (enableTrackEvaluatedIndividual && enableTrackIndividual) { throw ConfigProblemException("When tracking EvaluatedIndividual, it is not necessary to track individual") } @@ -1016,6 +1026,8 @@ class EMConfig { fun shouldGenerateRedisData() = generateRedisData + fun shouldGenerateDynamoDbData() = generateDynamoDbData + fun dtoSupportedForPayload() = dtoForRequestPayload && couldSupportDtoForPayload() fun couldSupportDtoForPayload() = problemType == ProblemType.REST && outputFormat.isJavaOrKotlin() @@ -2010,6 +2022,11 @@ class EMConfig { @DependsOnFalseFor("blackBox") var extractRedisExecutionInfo = false + @Experimental + @Cfg("Enable extracting DynamoDB execution info") + @DependsOnFalseFor("blackBox") + var extractDynamoDbExecutionInfo = false + @Experimental @Cfg("Enable EvoMaster to generate SQL data with direct accesses to the database. Use the Z3 SMT solver") @DependsOnFalseFor("blackBox") @@ -2070,6 +2087,11 @@ class EMConfig { @DependsOnFalseFor("blackBox") var generateRedisData = false + @Experimental + @Cfg("Enable EvoMaster to generate DynamoDB data with direct database access") + @DependsOnFalseFor("blackBox") + var generateDynamoDbData = false + @Cfg("When generating SQL data, how many new rows (max) to generate for each specific SQL Select") @Min(1.0) @DependsOnFalseFor("blackBox") diff --git a/core/src/main/kotlin/org/evomaster/core/problem/api/service/ApiWsStructureMutator.kt b/core/src/main/kotlin/org/evomaster/core/problem/api/service/ApiWsStructureMutator.kt index 98674ec1df..12f3d1d6ef 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/api/service/ApiWsStructureMutator.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/api/service/ApiWsStructureMutator.kt @@ -1,6 +1,7 @@ package org.evomaster.core.problem.api.service import com.google.inject.Inject +import org.evomaster.client.java.controller.api.dto.database.execution.DynamoDbFailedQuery import org.evomaster.client.java.controller.api.dto.database.execution.MongoFailedQuery import org.evomaster.client.java.controller.api.dto.database.execution.RedisFailedCommand import org.evomaster.client.java.instrumentation.shared.ExternalServiceSharedUtils @@ -16,6 +17,8 @@ import org.evomaster.core.problem.externalservice.httpws.service.HarvestActualHt import org.evomaster.core.problem.externalservice.httpws.service.HttpWsExternalServiceHandler import org.evomaster.core.database.redis.RedisDbAction import org.evomaster.core.database.redis.RedisInsertBuilder +import org.evomaster.core.database.dynamodb.DynamoDbAction +import org.evomaster.core.database.dynamodb.DynamoDbInsertBuilder import org.evomaster.core.search.EvaluatedIndividual import org.evomaster.core.search.GroupsOfChildren import org.evomaster.core.search.Individual @@ -195,6 +198,7 @@ abstract class ApiWsStructureMutator : StructureMutator() { addInitializingSqlActions(individual, mutatedGenes, sampler) addInitializingMongoDbActions(individual, mutatedGenes, sampler) addInitializingRedisDbActions(individual, mutatedGenes, sampler) + addInitializingDynamoDbActions(individual, mutatedGenes, sampler) addInitializingHostnameResolutionActions(individual, mutatedGenes, sampler) // TODO if we handle schedule actions with structure mutator } @@ -269,6 +273,40 @@ abstract class ApiWsStructureMutator : StructureMutator() { } } + private fun addInitializingDynamoDbActions( + individual: EvaluatedIndividual<*>, + mutatedGenes: MutatedGeneSpecification?, + sampler: ApiWsSampler + ) { + if (!config.shouldGenerateDynamoDbData()) { + return + } + + val ind = individual.individual as? T + ?: throw IllegalArgumentException("Invalid individual type") + + val failedQueries = individual.fitness.getViewOfAggregatedFailedDynamoDbQueries() + + if (failedQueries.isEmpty()) { + return + } + + val oldDynamoDbActions = mutableListOf().plus(ind.seeInitializingActions()) + + val addedDynamoDbInsertions = handleFailedDynamoDbQueries(ind, failedQueries) + .let { if (it.isEmpty()) emptyList() else listOf(it) } + + if (mutatedGenes != null && config.isEnabledArchiveGeneSelection()) { + individual.updateImpactGeneDueToAddedInitializationGenes( + mutatedGenes, + oldDynamoDbActions, + addedDynamoDbInsertions, + ImpactsOfIndividual.DYNAMODB_ACTION_KEY, + config + ) + } + } + private fun addInitializingHostnameResolutionActions( individual: EvaluatedIndividual<*>, mutatedGenes: MutatedGeneSpecification?, @@ -537,6 +575,31 @@ abstract class ApiWsStructureMutator : StructureMutator() { return addedActions } + private fun handleFailedDynamoDbQueries( + ind: T, + failedQueries: List + ): List { + + val existingKeys = ind.seeInitializingActions() + .filterIsInstance() + .map { it.insertionKey() } + .toSet() + + val addedActions = DynamoDbInsertBuilder.buildInsertActions( + failedQueries, + existingKeys + ) + + if (addedActions.isNotEmpty()) { + ind.addInitializingActions(actions = addedActions) + addedActions.forEach { action -> + action.seeTopGenes().forEach { gene -> gene.markAllAsInitialized() } + } + } + + return addedActions + } + private fun findMissing( fw: Map>, dbactions: List diff --git a/core/src/main/kotlin/org/evomaster/core/problem/enterprise/service/EnterpriseFitness.kt b/core/src/main/kotlin/org/evomaster/core/problem/enterprise/service/EnterpriseFitness.kt index 571a084c47..1ab12e37a9 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/enterprise/service/EnterpriseFitness.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/enterprise/service/EnterpriseFitness.kt @@ -14,6 +14,10 @@ import org.evomaster.core.database.redis.RedisDbAction import org.evomaster.core.database.redis.RedisDbActionResult import org.evomaster.core.database.redis.RedisDbActionTransformer import org.evomaster.core.database.redis.RedisExecution +import org.evomaster.core.database.dynamodb.DynamoDbAction +import org.evomaster.core.database.dynamodb.DynamoDbActionResult +import org.evomaster.core.database.dynamodb.DynamoDbActionTransformer +import org.evomaster.core.database.dynamodb.DynamoDbExecution import org.evomaster.core.database.sql.DatabaseExecution import org.evomaster.core.database.sql.SqlAction import org.evomaster.core.database.sql.SqlActionResult @@ -269,6 +273,20 @@ abstract class EnterpriseFitness : FitnessFunction() where T : Individual return true } + fun doDynamoDbCalls( + allDynamoDbActions: List, + actionResults: MutableList + ): Boolean { + if (allDynamoDbActions.isEmpty()) return true + val results = allDynamoDbActions.map { DynamoDbActionResult(it.getLocalId()) } + actionResults.addAll(results) + val execution = rc.executeDynamoDbInsertions(DynamoDbActionTransformer.transform(allDynamoDbActions)) + execution?.executionResults?.forEachIndexed { index, success -> + results.getOrNull(index)?.setInsertExecutionResult(success) + } + return execution?.executionResults?.all { it } ?: false + } + protected fun registerNewAction(action: Action, index: Int){ rc.registerNewAction(getActionDto(action, index)) } @@ -414,6 +432,13 @@ abstract class EnterpriseFitness : FitnessFunction() where T : Individual } fv.aggregateRedisDatabaseData() } + + if (configuration.extractDynamoDbExecutionInfo) { + for (i in 0 until dto.extraHeuristics.size) { + fv.setDynamoDbExecution(i, DynamoDbExecution.fromDto(dto.extraHeuristics[i].dynamoDbExecutionsDto)) + } + fv.aggregateDynamoDbData() + } } private fun handleSqlHeuristics( diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/ResourceRestFitness.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/ResourceRestFitness.kt index a9075b3536..b4126d13a7 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/ResourceRestFitness.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/ResourceRestFitness.kt @@ -16,6 +16,7 @@ import org.evomaster.core.problem.rest.resource.RestResourceCalls import org.evomaster.core.problem.rest.service.ResourceDepManageService import org.evomaster.core.problem.rest.service.ResourceManageService import org.evomaster.core.database.redis.RedisDbAction +import org.evomaster.core.database.dynamodb.DynamoDbAction import org.evomaster.core.search.action.ActionFilter import org.evomaster.core.search.action.ActionResult import org.evomaster.core.search.EvaluatedIndividual @@ -86,6 +87,8 @@ class ResourceRestFitness : AbstractRestFitness() { doRedisDbCalls(individual.seeInitializingActions().filterIsInstance(), actionResults) + doDynamoDbCalls(individual.seeInitializingActions().filterIsInstance(), actionResults) + //used for things like chaining "location" paths val chainState = mutableMapOf() diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/RestFitness.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/RestFitness.kt index 34624edfc0..cf489cd1ed 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/RestFitness.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/RestFitness.kt @@ -6,6 +6,7 @@ import org.evomaster.core.problem.httpws.auth.AuthUtils import org.evomaster.core.problem.rest.data.RestCallResult import org.evomaster.core.problem.rest.data.RestIndividual import org.evomaster.core.database.redis.RedisDbAction +import org.evomaster.core.database.dynamodb.DynamoDbAction import org.evomaster.core.search.action.ActionFilter import org.evomaster.core.search.action.ActionResult import org.evomaster.core.search.EvaluatedIndividual @@ -46,6 +47,7 @@ open class RestFitness : AbstractRestFitness() { doDbCalls(individual.seeInitializingActions().filterIsInstance(), actionResults = actionResults) doMongoDbCalls(individual.seeInitializingActions().filterIsInstance(), actionResults = actionResults) doRedisDbCalls(individual.seeInitializingActions().filterIsInstance(), actionResults = actionResults) + doDynamoDbCalls(individual.seeInitializingActions().filterIsInstance(), actionResults = actionResults) val fv = FitnessValue(individual.size().toDouble()) diff --git a/core/src/main/kotlin/org/evomaster/core/search/EvaluatedIndividual.kt b/core/src/main/kotlin/org/evomaster/core/search/EvaluatedIndividual.kt index 6921d424ba..42e2ca1d56 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/EvaluatedIndividual.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/EvaluatedIndividual.kt @@ -19,6 +19,7 @@ import org.evomaster.core.problem.rest.data.RestCallResult import org.evomaster.core.problem.rest.data.RestIndividual import org.evomaster.core.problem.rest.resource.ResourceImpactOfIndividual import org.evomaster.core.database.redis.RedisDbAction +import org.evomaster.core.database.dynamodb.DynamoDbAction import org.evomaster.core.scheduletask.ScheduleTaskAction import org.evomaster.core.search.action.* import org.evomaster.core.search.action.ActionFilter.* @@ -1007,7 +1008,7 @@ class EvaluatedIndividual( } private fun initializingActionClasses(): List> { - return listOf(MongoDbAction::class, SqlAction::class, RedisDbAction::class, ScheduleTaskAction::class) + return listOf(MongoDbAction::class, SqlAction::class, RedisDbAction::class, DynamoDbAction::class, ScheduleTaskAction::class) } fun hasAnyPotentialFault() = this.fitness.hasAnyPotentialFault(this.individual.searchGlobalState!!.idMapper) diff --git a/core/src/main/kotlin/org/evomaster/core/search/FitnessValue.kt b/core/src/main/kotlin/org/evomaster/core/search/FitnessValue.kt index 13a4eec210..67b17f8d03 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/FitnessValue.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/FitnessValue.kt @@ -2,9 +2,11 @@ package org.evomaster.core.search import com.webfuzzing.commons.faults.DefinedFaultCategory import org.evomaster.client.java.controller.api.dto.BootTimeInfoDto +import org.evomaster.client.java.controller.api.dto.database.execution.DynamoDbFailedQuery import org.evomaster.client.java.controller.api.dto.database.execution.MongoFailedQuery import org.evomaster.client.java.controller.api.dto.database.execution.RedisFailedCommand import org.evomaster.core.EMConfig +import org.evomaster.core.database.dynamodb.DynamoDbExecution import org.evomaster.core.database.sql.DatabaseExecution import org.evomaster.core.EMConfig.SecondaryObjectiveStrategy.* import org.evomaster.core.database.mongo.MongoExecution @@ -83,6 +85,8 @@ class FitnessValue( val redisExecutions: MutableMap = mutableMapOf() + val dynamoDbExecutions: MutableMap = mutableMapOf() + /** * When SUT does SQL commands using WHERE, keep track of when those "fails" (ie evaluate * to false), in particular, the tables and columns in them involved @@ -107,6 +111,8 @@ class FitnessValue( */ private val aggregatedFailedRedisCommands: MutableList = mutableListOf() + private val aggregatedFailedDynamoDbQueries: MutableList = mutableListOf() + /** * To keep track of accessed external services prevent from adding them again * TODO: This is not completed, not need to consider for review for now @@ -139,9 +145,11 @@ class FitnessValue( copy.databaseExecutions.putAll(this.databaseExecutions) //note: DatabaseExecution supposed to be immutable copy.mongoExecutions.putAll(this.mongoExecutions) copy.redisExecutions.putAll(this.redisExecutions) + copy.dynamoDbExecutions.putAll(this.dynamoDbExecutions) copy.aggregateDatabaseData() copy.aggregateMongoDatabaseData() copy.aggregateRedisDatabaseData() + copy.aggregateDynamoDbData() copy.executionTimeMs = executionTimeMs copy.accessedExternalServiceRequests.putAll(this.accessedExternalServiceRequests) copy.accessedDefaultWM.putAll(this.accessedDefaultWM.toMap()) @@ -189,6 +197,11 @@ class FitnessValue( redisExecutions.values.map { it.failedCommands?.let { it1 -> aggregatedFailedRedisCommands.addAll(it1) } } } + fun aggregateDynamoDbData() { + aggregatedFailedDynamoDbQueries.clear() + dynamoDbExecutions.values.forEach { aggregatedFailedDynamoDbQueries.addAll(it.failedQueries) } + } + fun addExtraObjectivesToMinimize(actionIndex: Int, list: List) { if (extraToMinimize[actionIndex] == null) { extraToMinimize[actionIndex] = list.sorted() @@ -210,6 +223,10 @@ class FitnessValue( redisExecutions[actionIndex] = redisExecution } + fun setDynamoDbExecution(actionIndex: Int, execution: DynamoDbExecution) { + dynamoDbExecutions[actionIndex] = execution + } + fun isAnyDatabaseExecutionInfo() = databaseExecutions.isNotEmpty() fun getViewOfData(): Map { @@ -224,6 +241,8 @@ class FitnessValue( fun getViewOfAggregatedFailedRedisCommands() = aggregatedFailedRedisCommands + fun getViewOfAggregatedFailedDynamoDbQueries() = aggregatedFailedDynamoDbQueries + fun doesCover(target: Int): Boolean { return targets[target]?.score == MAX_VALUE } diff --git a/core/src/main/kotlin/org/evomaster/core/search/impact/impactinfocollection/ImpactsOfIndividual.kt b/core/src/main/kotlin/org/evomaster/core/search/impact/impactinfocollection/ImpactsOfIndividual.kt index c6103ea119..bb0271e2af 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/impact/impactinfocollection/ImpactsOfIndividual.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/impact/impactinfocollection/ImpactsOfIndividual.kt @@ -4,6 +4,7 @@ import org.evomaster.core.database.sql.SqlAction import org.evomaster.core.database.mongo.MongoDbAction import org.evomaster.core.problem.externalservice.HostnameResolutionAction import org.evomaster.core.database.redis.RedisDbAction +import org.evomaster.core.database.dynamodb.DynamoDbAction import org.evomaster.core.search.action.EnvironmentAction import org.evomaster.core.search.action.Action import org.evomaster.core.search.FitnessValue @@ -85,6 +86,8 @@ open class ImpactsOfIndividual( val REDISDB_ACTION_KEY = RedisDbAction::class.java.name + val DYNAMODB_ACTION_KEY = DynamoDbAction::class.java.name + val HOSTNAME_RESOLUTION_KEY = HostnameResolutionAction::class.java.name } diff --git a/core/src/test/kotlin/org/evomaster/core/TestUtils.kt b/core/src/test/kotlin/org/evomaster/core/TestUtils.kt index 5b26ce679f..3f0ce4689a 100644 --- a/core/src/test/kotlin/org/evomaster/core/TestUtils.kt +++ b/core/src/test/kotlin/org/evomaster/core/TestUtils.kt @@ -3,7 +3,10 @@ package org.evomaster.core import org.evomaster.core.search.Individual import org.evomaster.core.search.service.Randomness +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbScalarTypeDto import org.evomaster.client.java.controller.api.dto.database.schema.DatabaseType +import org.evomaster.core.database.dynamodb.DynamoDbAction +import org.evomaster.core.database.dynamodb.DynamoDbAttributeGene import org.evomaster.core.problem.api.param.Param import org.evomaster.core.database.sql.SqlAction import org.evomaster.core.database.sql.schema.Column @@ -84,6 +87,20 @@ object TestUtils { return listOf(fooInsertion, barInsertion) } + /** Generate a DynamoDB action with one string attribute for unit testing. */ + fun generateFakeDynamoDbAction(tableName: String, attributeName: String, attributeValue: String): DynamoDbAction { + return DynamoDbAction( + tableName, + listOf( + DynamoDbAttributeGene( + attributeName, + DynamoDbScalarTypeDto.STRING, + StringGene(attributeName, attributeValue) + ) + ) + ) + } + fun generateFakeQueryRestAction(id: String, pathString: String, onlyId : Boolean = false) : RestCallAction { val queryNameParam = QueryParam("name", StringGene("name")) val queryIdParam = QueryParam("id", IntegerGene("id")) diff --git a/core/src/test/kotlin/org/evomaster/core/problem/rest/individual/RestIndividualTest.kt b/core/src/test/kotlin/org/evomaster/core/problem/rest/individual/RestIndividualTest.kt index 24aebbc988..a5314e8759 100644 --- a/core/src/test/kotlin/org/evomaster/core/problem/rest/individual/RestIndividualTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/problem/rest/individual/RestIndividualTest.kt @@ -4,6 +4,7 @@ import org.evomaster.core.TestUtils import org.evomaster.core.problem.enterprise.SampleType import org.evomaster.core.problem.rest.data.RestIndividual import org.evomaster.core.problem.rest.resource.RestResourceCalls +import org.evomaster.core.search.action.ActionFilter import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test @@ -20,10 +21,18 @@ class RestIndividualTest { val twoDbActions = TestUtils.generateTwoFakeDbActions(1001L, 1002L, 12345L, 10L, "Foo", "Bar", 0, 42) val fooAction = TestUtils.generateFakeQueryRestAction("1", "/foo") val barAction = TestUtils.generateFakeQueryRestAction("2", "/bar", true) + val dynamoDbAction = TestUtils.generateFakeDynamoDbAction("WorldCupPlayers", "country", "Argentina") + val fooResource = RestResourceCalls( + actions = listOf(fooAction), + sqlActions = listOf(twoDbActions[0]) + ).apply { addChild(dynamoDbAction) } + assertEquals(listOf(dynamoDbAction), fooResource.seeActions(ActionFilter.ONLY_DYNAMODB)) + assertTrue(fooResource.seeActions(ActionFilter.INIT).contains(dynamoDbAction)) + assertTrue(fooResource.seeActions(ActionFilter.ONLY_DB).contains(dynamoDbAction)) val fakeInd = RestIndividual( mutableListOf( - RestResourceCalls(actions = listOf(fooAction), sqlActions = listOf(twoDbActions[0])), + fooResource, RestResourceCalls(actions = listOf(barAction), sqlActions = listOf(twoDbActions[1])) ), SampleType.RANDOM @@ -40,8 +49,9 @@ class RestIndividualTest { fakeInd.ensureFlattenedStructure() assertTrue(fakeInd.getResourceCalls().isEmpty()) - assertEquals(2, fakeInd.seeInitializingActions().size) + assertEquals(3, fakeInd.seeInitializingActions().size) + assertEquals(listOf(dynamoDbAction), fakeInd.seeDynamoDbActions()) assertEquals(2, fakeInd.seeMainExecutableActions().size) } -} \ No newline at end of file +} diff --git a/docs/options.md b/docs/options.md index 5140f347cd..e240f4c38f 100644 --- a/docs/options.md +++ b/docs/options.md @@ -305,7 +305,9 @@ There are 3 types of options: |`externalRequestResponseSelectionStrategy`| __Enum__. Harvested external request response selection strategy. *Valid values*: `EXACT, CLOSEST_SAME_DOMAIN, CLOSEST_SAME_PATH, RANDOM`. *Default value*: `EXACT`.| |`externalServiceIP`| __String__. User provided external service IP. When EvoMaster mocks external services, mock server instances will run on local addresses starting from this provided address. Min value is 127.0.0.4. Lower values like 127.0.0.2 and 127.0.0.3 are reserved. *Constraints*: `regex (?!^0*127(\.0*0){2}\.0*[0123]$)^0*127(\.0*(25[0-5]\|2[0-4][0-9]\|1?[0-9]?[0-9])){3}$`. *Default value*: `127.0.0.4`.| |`externalServiceIPSelectionStrategy`| __Enum__. Specify a method to select the first external service spoof IP address. *Valid values*: `NONE, DEFAULT, USER, RANDOM`. *Default value*: `NONE`.| +|`extractDynamoDbExecutionInfo`| __Boolean__. Enable extracting DynamoDB execution info. *Depends on*: `blackBox=false`. *Default value*: `false`.| |`extractRedisExecutionInfo`| __Boolean__. Enable extracting Redis execution info. *Depends on*: `blackBox=false`. *Default value*: `false`.| +|`generateDynamoDbData`| __Boolean__. Enable EvoMaster to generate DynamoDB data with direct database access. *Depends on*: `blackBox=false`. *Default value*: `false`.| |`generateRedisData`| __Boolean__. Enable EvoMaster to generate Redis data with direct accesses to the database. *Depends on*: `blackBox=false`. *Default value*: `false`.| |`generateSqlDataWithZ3`| __Boolean__. Enable EvoMaster to generate SQL data with direct accesses to the database. Use the Z3 SMT solver. *Depends on*: `blackBox=false`. *Default value*: `false`.| |`handleFlakiness`| __Boolean__. Specify whether to detect flakiness and handle the flakiness in assertions during post handling of fuzzing. Note that flakiness is now supported only for fuzzing REST APIs. *Default value*: `false`.|