-
Notifications
You must be signed in to change notification settings - Fork 95
Phg/dto kickoff #1249
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Phg/dto kickoff #1249
Changes from all commits
108e716
3a25749
bc380c2
7b766f5
ae30fe6
4fd15b6
423cb4f
86fbc05
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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, | ||
) |
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 |
---|---|---|
|
@@ -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 | ||
|
@@ -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) | ||
|
@@ -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 -> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you could move the code here under some utility function in the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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) { | ||
Pgarrett marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// TODO: Determine strategy for objects that are not defined as a component and do not have a name | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could use something related to There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you will need to handle the cases of There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe it's something the Gene could answer by itself, something like: 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}") | ||
}) | ||
} | ||
|
||
} |
Uh oh!
There was an error while loading. Please reload this page.