Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions core/src/main/kotlin/org/evomaster/core/EMConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2895,6 +2895,13 @@ class EMConfig {
@Min(1.0)
var asyncApiReplyTimeoutMs = 5000

@Experimental
@Cfg("When testing an AsyncAPI service, the probability of publishing a message's declared example as it is," +
" rather than a value sampled from its schema. A service that silently drops what it does not recognise" +
" may never reply to sampled values, and the examples are what its author knows it accepts.")
@Probability(true)
var probAsyncApiExamples = 0.0

@Cfg("Whether to enable customized responses indicating business logic")
var enableRPCCustomizedResponseTargets = true

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ object AsyncApiGeneBuilder {
private const val TYPE_NUMBER = "number"
private const val TYPE_BOOLEAN = "boolean"

/*
What this builder reads off a Message Example Object: the two parts a gene is built for,
and the name that lets the search keep a whole example together.
*/
private const val EXAMPLE_PAYLOAD = "payload"
private const val EXAMPLE_HEADERS = "headers"
private const val EXAMPLE_NAME = "name"

/**
* The keywords whose value is literal data rather than a schema, so nothing inside them is
* a keyword either.
Expand All @@ -70,12 +78,21 @@ object AsyncApiGeneBuilder {

/**
* The genes for a message's payload, or null when it declares none.
*
* When the message declares examples and [options] ask for them, their payloads are offered
* as whole values beside the schema-derived genes, with the probability the options give.
*/
fun buildPayloadGene(
schema: AsyncApiDocument,
message: AsyncApiMessage,
options: RestActionBuilderV3.Options
): Gene? = build(message.payload, "${message.id}.payload", schema, options)
): Gene? = build(
message.payload,
"${message.id}.payload",
schema,
options,
examplesOf(message, EXAMPLE_PAYLOAD)
)

/**
* The genes for a message's headers, or null when it declares none.
Expand All @@ -98,7 +115,48 @@ object AsyncApiGeneBuilder {
schema: AsyncApiDocument,
message: AsyncApiMessage,
options: RestActionBuilderV3.Options
): Gene? = build(withoutCorrelationId(message), "${message.id}.headers", schema, options)
): Gene? = build(
withoutCorrelationId(message),
"${message.id}.headers",
schema,
options,
examplesOf(message, EXAMPLE_HEADERS).map { (headers, name) ->
Pair(withoutCorrelationField(headers, message), name)
}
)

/**
* The [part] of every example the message declares that has one, each with the example's
* name where it gives one.
*
* They go to the gene builder beside the schema, the way REST passes a parameter's
* examples, rather than written into it: that path never crosses the OpenAPI parser, which
* would keep only one, and draws no complaint on a scalar. The name is what lets the search
* pick a whole example consistently across fields, when asked to with probNamedExamples.
*/
private fun examplesOf(message: AsyncApiMessage, part: String): List<Pair<JsonNode, String?>> =
message.examples.mapNotNull { example ->
example.get(part)
?.takeUnless { it.isNull }
?.let { Pair(it, example.get(EXAMPLE_NAME)?.takeIf { n -> n.isTextual }?.asText()) }
}

/**
* The example headers without the one the correlation id is stamped into, which the headers
* gene does not have either.
*/
private fun withoutCorrelationField(example: JsonNode, message: AsyncApiMessage): JsonNode {

val correlation = message.correlationId
val field = correlation?.fieldName

if (correlation == null || correlation.source != AsyncApiCorrelationId.Source.HEADER
|| field == null || !example.isObject) {
return example
}

return (example.deepCopy<JsonNode>() as ObjectNode).apply { remove(field) }
}

/**
* The headers schema without the property the correlation id is stamped into.
Expand Down Expand Up @@ -146,6 +204,9 @@ object AsyncApiGeneBuilder {
/**
* Options for building AsyncAPI payloads.
*
* `probUseExamples` is the probability of publishing a message's declared example as it is;
* it is off unless the user asks, since it is a way of steering the search.
*
* Note `invalidData = false`, which is not what REST does. That flag makes the builder add
* a bogus "EVOMASTER" member to every enum, on purpose, to probe how a service handles a
* value it never declared. In a message payload that backfires: an enum of one value is how
Expand All @@ -160,6 +221,7 @@ object AsyncApiGeneBuilder {
fun options(config: EMConfig) = RestActionBuilderV3.Options(
enableConstraintHandling = config.enableSchemaConstraintHandling,
invalidData = false,
probUseExamples = config.probAsyncApiExamples,
usingWhiteBox = !config.blackBox,
enableAdvancedFormats = config.enableAdvancedFormats,
inferFormatFromNames = config.inferFormatFromNames
Expand All @@ -169,7 +231,8 @@ object AsyncApiGeneBuilder {
declared: JsonNode?,
inlineName: String,
schema: AsyncApiDocument,
options: RestActionBuilderV3.Options
options: RestActionBuilderV3.Options,
examples: List<Pair<Any, String?>>
): Gene? {

if (declared == null) {
Expand Down Expand Up @@ -201,7 +264,7 @@ object AsyncApiGeneBuilder {
}

//the format createGeneForDTO expects: the name of the wanted schema, then all of them
return RestActionBuilderV3.createGeneForDTO(name, "\"$name\":$schemas", options)
return RestActionBuilderV3.createGeneForDTO(name, "\"$name\":$schemas", options, examples)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,6 @@ class AsyncApiSampler : ApiWsSampler<AsyncApiIndividual>() {
return createIndividual(SampleType.RANDOM, actions)
}

/*
TODO Message examples (AsyncApiMessage.getExamples) are parsed and never read. Sampling
from them some of the time, as REST does with probRestExamples, would start the search
from payloads the author knows the service accepts.
*/
/**
* A copy of one of the action templates, chosen at random, with its genes initialized.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,12 +293,17 @@ object RestActionBuilderV3 {
* @throws IllegalArgumentException if the provided 'name' is not found at the beginning of 'allSchemas'.
* @throws IllegalStateException if the schema with the specified 'name' cannot be found in 'allSchemas'.
*
* @param examples values to offer for the wanted schema as a whole, each with an optional
* name, beside the genes derived from it. They take the same path as a REST
* parameter's examples, so they never go through the OpenAPI parser, and as
* for those, none is offered unless [Options.probUseExamples] gives it a chance.
* @see Gene
* @see Options
*/
fun createGeneForDTO(dtoSchemaName: String,
allSchemas: String,
options: Options
options: Options,
examples: List<Pair<Any,String?>> = listOf()
) : Gene{
if(!allSchemas.startsWith("\"$dtoSchemaName\"")){
throw IllegalArgumentException("Invalid name $dtoSchemaName for schema $allSchemas")
Expand All @@ -309,7 +314,14 @@ object RestActionBuilderV3 {
val schemas = getMapStringFromSchemas(allSchemasValue)
val dtoSchema = schemas[dtoSchemaName] ?: throw IllegalStateException("cannot find the schema with $dtoSchemaName from $allSchemas")

if(dtoCache.containsKey(dtoSchema)){
//gated here rather than left to the assembler, which would wrap them in a choice weighted at zero
val offered = if (options.probUseExamples > 0) examples else listOf()

/*
The cache is keyed on the schema text alone, so it can only serve a call that brings
no examples: two callers may share a schema and still offer different examples for it.
*/
if(offered.isEmpty() && dtoCache.containsKey(dtoSchema)){
return dtoCache[dtoSchema]!!.copy()
}

Expand All @@ -328,19 +340,28 @@ object RestActionBuilderV3 {
val currentSchema = SchemaOpenAPI(schema,swagger.openAPI, SchemaLocation.MEMORY)
val schemaHolder = RestSchema(currentSchema)

var wanted: Gene? = null

schemas.forEach { (t, u) ->
val forThisOne = if (t == dtoSchemaName) offered else listOf()
val gene = getGene(t,
swagger.openAPI.components.schemas[t]!!,
schemaHolder,
currentSchema,
ArrayDeque(),
t,
options,
examples = forThisOne,
messages = mutableListOf())
dtoCache[u] = gene
if (forThisOne.isEmpty()) {
dtoCache[u] = gene
}
if (t == dtoSchemaName) {
wanted = gene
}
}

return dtoCache[dtoSchema]!!.copy()
return (wanted ?: throw IllegalStateException("No gene was built for $dtoSchemaName")).copy()
}

/**
Expand Down
Loading
Loading