Skip to content
Merged
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
5 changes: 5 additions & 0 deletions core/src/main/kotlin/org/evomaster/core/EMConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2495,6 +2495,11 @@ class EMConfig {
" on a single line")
var maxLengthForCommentLine = 80

@Experimental
@Cfg("In REST APIs, when request Content-Type is JSON, POJOs are used instead of raw JSON string. " +
"Only available for JVM languages")
var dtoForRequestPayload = false

fun getProbabilityUseDataPool() : Double{
return if(blackBox){
bbProbabilityUseDataPool
Expand Down
5 changes: 5 additions & 0 deletions core/src/main/kotlin/org/evomaster/core/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,11 @@ class Main {

val writer = injector.getInstance(TestSuiteWriter::class.java)

// TODO: support Kotlin for DTOs
if (config.problemType == EMConfig.ProblemType.REST && config.dtoForRequestPayload && config.outputFormat.isJava()) {
writer.writeDtos(solution.getFileName().name)
}

val splitResult = TestSuiteSplitter.split(solution, config)

val suites = splitResult.splitOutcome.map {
Expand Down
11 changes: 11 additions & 0 deletions core/src/main/kotlin/org/evomaster/core/output/dto/DtoClass.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.evomaster.core.output.dto

class DtoClass(
val name: String,
val fields: MutableList<DtoField> = mutableListOf()) {

fun addField(field: DtoField) {
fields.add(field)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.evomaster.core.output.dto

class DtoField(
val name: String,
val type: String,
)
109 changes: 109 additions & 0 deletions core/src/main/kotlin/org/evomaster/core/output/dto/JavaDtoWriter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package org.evomaster.core.output.dto

import org.evomaster.core.output.Lines
import org.evomaster.core.output.OutputFormat
import org.evomaster.core.output.TestSuiteFileName
import org.evomaster.core.utils.StringUtils
import java.nio.file.Files
import java.nio.file.Path

/**
* When [EMConfig.dtoForRequestPayload] is enabled and [OutputFormat] is set to Java, this writer object will
* create DTO java classes in the filesystem under the `dto` package. These DTO classes will then be used for
* test case writing for JSON request payloads. Instead of having a stringified view of the payload, EM will
* leverage these DTOs.
*/
object JavaDtoWriter {

/**
* @param testSuitePath under which the java class must be written
* @param outputFormat forwarded to the [Lines] helper class and for setting the .java extension in the generated file
* @param dtoClass to be written to filesystem
*/
fun write(testSuitePath: Path, outputFormat: OutputFormat, dtoClass: DtoClass) {
val dtoFilename = TestSuiteFileName(appendDtoPackage(dtoClass.name))
val lines = Lines(outputFormat)
setPackage(lines)
addImports(lines)
initClass(lines, dtoFilename.getClassName())
addClassContent(lines, dtoClass)
closeClass(lines)
saveToDisk(lines.toString(), getTestSuitePath(testSuitePath, dtoFilename, outputFormat))
}

private fun setPackage(lines: Lines) {
lines.add("package dto;")
lines.addEmpty()
}

private fun addImports(lines: Lines) {
lines.add("import java.util.Optional;")
lines.addEmpty()
lines.add("import com.fasterxml.jackson.annotation.JsonInclude;")
lines.add("import shaded.com.fasterxml.jackson.annotation.JsonProperty;")
lines.addEmpty()
}

private fun initClass(lines: Lines, dtoFilename: String) {
lines.add("@JsonInclude(JsonInclude.Include.NON_NULL)")
lines.add("public class $dtoFilename {")
lines.addEmpty()
}

private fun addClassContent(lines: Lines, dtoClass: DtoClass) {
addVariables(lines, dtoClass)
addGettersAndSetters(lines, dtoClass)
}

private fun addVariables(lines: Lines, dtoClass: DtoClass) {
dtoClass.fields.forEach {
lines.indented {
lines.add("@JsonProperty(\"${it.name}\")")
lines.add("private Optional<${it.type}> ${it.name};")
}
lines.addEmpty()
}
}

private fun addGettersAndSetters(lines: Lines, dtoClass: DtoClass) {
dtoClass.fields.forEach {
val varName = it.name
val varType = it.type
val capitalizedVarName = StringUtils.capitalization(varName)
lines.indented {
lines.add("public Optional<${varType}> get${capitalizedVarName}() {")
lines.indented {
lines.add("return ${varName};")
}
lines.add("}")
lines.addEmpty()
lines.add("public void set${capitalizedVarName}(${varType} ${varName}) {")
lines.indented {
lines.add("this.${varName} = Optional.ofNullable(${varName});")
}
lines.add("}")
}
lines.addEmpty()
}
}

private fun closeClass(lines: Lines) {
lines.add("}")
}

private fun appendDtoPackage(name: String): String {
return "dto.$name"
}

private fun getTestSuitePath(testSuitePath: Path, dtoFilename: TestSuiteFileName, outputFormat: OutputFormat) : Path{
return testSuitePath.resolve(dtoFilename.getAsPath(outputFormat))
}

private fun saveToDisk(testFileContent: String, path: Path) {
Files.createDirectories(path.parent)
Files.deleteIfExists(path)
Files.createFile(path)

path.toFile().appendText(testFileContent)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,33 @@ import org.evomaster.core.EMConfig
import org.evomaster.core.output.*
import org.evomaster.core.output.TestWriterUtils.getWireMockVariableName
import org.evomaster.core.output.TestWriterUtils.handleDefaultStubForAsJavaOrKotlin
import org.evomaster.core.output.dto.DtoClass
import org.evomaster.core.output.dto.DtoField
import org.evomaster.core.output.dto.JavaDtoWriter
import org.evomaster.core.output.naming.NumberedTestCaseNamingStrategy
import org.evomaster.core.output.naming.TestCaseNamingStrategyFactory
import org.evomaster.core.problem.api.ApiWsIndividual
import org.evomaster.core.problem.externalservice.httpws.HttpWsExternalService
import org.evomaster.core.problem.externalservice.httpws.HttpExternalServiceAction
import org.evomaster.core.problem.externalservice.httpws.service.HttpWsExternalServiceHandler
import org.evomaster.core.problem.rest.BlackBoxUtils
import org.evomaster.core.problem.rest.param.BodyParam
import org.evomaster.core.problem.rest.data.RestIndividual
import org.evomaster.core.problem.rest.service.sampler.AbstractRestSampler
import org.evomaster.core.remote.service.RemoteController
import org.evomaster.core.search.Solution
import org.evomaster.core.search.gene.BooleanGene
import org.evomaster.core.search.gene.Gene
import org.evomaster.core.search.gene.ObjectGene
import org.evomaster.core.search.gene.datetime.DateGene
import org.evomaster.core.search.gene.datetime.TimeGene
import org.evomaster.core.search.gene.numeric.DoubleGene
import org.evomaster.core.search.gene.numeric.FloatGene
import org.evomaster.core.search.gene.numeric.IntegerGene
import org.evomaster.core.search.gene.numeric.LongGene
import org.evomaster.core.search.gene.string.Base64StringGene
import org.evomaster.core.search.gene.string.StringGene
import org.evomaster.core.search.gene.utils.GeneUtils
import org.evomaster.core.search.service.Sampler
import org.evomaster.core.search.service.SearchTimeController
import org.evomaster.test.utils.EMTestUtils
Expand Down Expand Up @@ -201,6 +218,14 @@ class TestSuiteWriter {
)
}

// TODO: take DTO extraction and writing to a different class
fun writeDtos(solutionFilename: String) {
val testSuitePath = getTestSuitePath(TestSuiteFileName(solutionFilename), config).parent
getDtos().forEach {
JavaDtoWriter.write(testSuitePath, config.outputFormat, it)
}
}

private fun handleResetDatabaseInput(solution: Solution<*>): String {
if (!config.outputFormat.isJavaOrKotlin())
throw IllegalStateException("DO NOT SUPPORT resetDatabased for " + config.outputFormat)
Expand Down Expand Up @@ -1071,4 +1096,52 @@ class TestSuiteWriter {
.toList()
}

private fun getDtos(): List<DtoClass> {
val restSampler = sampler as AbstractRestSampler
val result = mutableListOf<DtoClass>()
restSampler.getActionDefinitions().forEach { action ->
Copy link
Collaborator

Choose a reason for hiding this comment

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

you could move the code here under some utility function in the dto package, and then call it with restSampler.getActionDefinitions() as input

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, in the future I was actually thinking of creating a new dedicated class for this, but for starters I added here in the TestSuiteWriter. The class-to-be would actually have to run before tests are split, so as to avoid re-writing or even overwriting DTOs. Then the generated DTOs should be shared with the TestCaseWriter, not sure how yet

action.getViewOfChildren().forEach { child ->
if (child is BodyParam) {
val primaryGene = GeneUtils.getWrappedValueGene(child.primaryGene())
// TODO: Payloads could also be json arrays, analyze ArrayGene
if (primaryGene is ObjectGene) {
// TODO: Determine strategy for objects that are not defined as a component and do not have a name
Copy link
Collaborator

Choose a reason for hiding this comment

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

could use something related to action.getName, and put this as a comment

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Could be, yes. We'll most likely come back to this later on

val dtoClass = DtoClass(primaryGene.refType?:TestWriterUtils.safeVariableName(action.getName()))
primaryGene.fixedFields.forEach { field ->
try {
dtoClass.addField(getDtoField(field))
} catch (ex: Exception) {
log.warn("A failure has occurred when collecting DTOs. \n"
+ "Exception: ${ex.localizedMessage} \n"
+ "At ${ex.stackTrace.joinToString(separator = " \n -> ")}. "
)
assert(false)
}
}
result.add(dtoClass)
}
}
}
}
return result
}

private fun getDtoField(field: Gene): DtoField {
val wrappedGene = GeneUtils.getWrappedValueGene(field)
return DtoField(field.name, when (wrappedGene) {
// TODO: handle nested arrays, objects and extend type system for dto fields
Copy link
Collaborator

Choose a reason for hiding this comment

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

you will need to handle the cases of OptionalGene and NullableGene (see how wrapping works), as that will impact what kinds of field types you will need to create. also RegexGene is going to be common. anyway, you will see what things crashing when we activate this feature in the E2E... :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

As a matter of fact, we need more E2E tests! Most e2e tests check on tests generated using GET:

  • GET: 103+ matches in 36+ files
  • POST: 67 matches in 36 files
  • PUT: 6 matches in 5 files

And I looking into the POST tests, some do not have a DTO request payload but instead a response one, or are using simple cases. Looked into that for debugging

is StringGene -> "String"
is IntegerGene -> "Integer"
is LongGene -> "Long"
is DoubleGene -> "Double"
is FloatGene -> "Float"
is Base64StringGene -> "String"
// Time and Date genes will be handled with strings at the moment. In the future we'll evaluate if it's worth having any validation
is DateGene -> "String"
is TimeGene -> "String"
Copy link
Collaborator

Choose a reason for hiding this comment

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

you might want to use special types for this, and, in the DTOs, throw an IllegalArgumentExcpetion if invalid string. however, for now can leave it as acomment (as we still create invalid strings outside robustness testing, and that is not fixed yet)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Maybe it's something the Gene could answer by itself, something like: gene.getDtoType() and have that answer back. That would also save us from these giant ifs

One other question I have is, for these specific cases of Time and Date, should we in the future handle them with jodatime objects or similar? Or just set them as strings and keep it simple? For a start I think strings are fine, not sure what you had in mind for the future

Copy link
Collaborator

Choose a reason for hiding this comment

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

for now we can keep it simple. then we can see feedback from test engineers at VW

is BooleanGene -> "Boolean"
else -> throw Exception("Not supported gene at the moment: ${wrappedGene?.javaClass?.simpleName}")
})
}

}
1 change: 1 addition & 0 deletions docs/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ There are 3 types of options:
|`discoveredInfoRewardedInFitness`| __Boolean__. If there is new discovered information from a test execution, reward it in the fitness function. *Default value*: `false`.|
|`dockerLocalhost`| __Boolean__. Replace references to 'localhost' to point to the actual host machine. Only needed when running EvoMaster inside Docker. *Default value*: `false`.|
|`dpcTargetTestSize`| __Int__. Specify a max size of a test to be targeted when either DPC_INCREASING or DPC_DECREASING is enabled. *Default value*: `1`.|
|`dtoForRequestPayload`| __Boolean__. In REST APIs, when request Content-Type is JSON, POJOs are used instead of raw JSON string. Only available for JVM languages. *Default value*: `false`.|
|`employResourceSizeHandlingStrategy`| __Enum__. Specify a strategy to determinate a number of resources to be manipulated throughout the search. *Valid values*: `NONE, RANDOM, DPC`. *Default value*: `NONE`.|
|`enableAdaptiveResourceStructureMutation`| __Boolean__. Specify whether to decide the resource-based structure mutator and resource to be mutated adaptively based on impacts during focused search.Note that it only works when resource-based solution is enabled for solving REST problem. *Default value*: `false`.|
|`enableCustomizedMethodForMockObjectHandling`| __Boolean__. Whether to apply customized method (i.e., implement 'customizeMockingRPCExternalService' for external services or 'customizeMockingDatabase' for database) to handle mock object. *Default value*: `false`.|
Expand Down