Skip to content

Commit 546673c

Browse files
authored
Merge pull request #1551 from WebFuzzing/config-enum-set
introducing set and list of enums for options in EMConfig
2 parents a74d114 + cf94f41 commit 546673c

5 files changed

Lines changed: 124 additions & 52 deletions

File tree

core/src/main/kotlin/org/evomaster/core/EMConfig.kt

Lines changed: 91 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import org.evomaster.core.problem.enterprise.ExperimentalFaultCategory
1919
import org.evomaster.core.search.impact.impactinfocollection.GeneMutationSelectionMethod
2020
import org.evomaster.core.search.service.IdMapper
2121
import org.slf4j.LoggerFactory
22+
import java.lang.reflect.ParameterizedType
2223
import java.net.MalformedURLException
2324
import java.net.URL
2425
import java.nio.file.Files
@@ -235,17 +236,27 @@ class EMConfig {
235236
}
236237
}
237238

238-
var experimentalValues = ""
239-
var validValues = ""
240-
val returnType = m.returnType.javaType as Class<*>
241-
242-
if (returnType.isEnum) {
243-
val elements = returnType.getDeclaredMethod("values")
244-
.invoke(null) as Array<*>
245-
val experimentElements = elements.filter { it is WithExperimentalOptions && it.isExperimental() }
246-
val validElements = elements.filter { it !is WithExperimentalOptions || !it.isExperimental() }
247-
experimentalValues = experimentElements.joinToString(", ")
248-
validValues = validElements.joinToString(", ")
239+
val returnType = m.returnType.javaType
240+
241+
val (validValues, experimentalValues) = when (returnType) {
242+
is Class<*> if returnType.isEnum -> {
243+
getValidAndExperimentalEnumValues(returnType)
244+
}
245+
246+
is ParameterizedType -> {
247+
if (!Collection::class.java.isAssignableFrom(returnType.rawType as Class<*>)) {
248+
throw IllegalStateException("Configuration '${m.name}' is parameterized, but not a collection")
249+
}
250+
if (returnType.actualTypeArguments.size != 1) {
251+
throw IllegalStateException("Configuration '${m.name}' has more than 1 generic type in a collection")
252+
}
253+
val generic = returnType.actualTypeArguments[0] as Class<*>
254+
getValidAndExperimentalEnumValues(generic)
255+
}
256+
257+
else -> {
258+
Pair("", "")
259+
}
249260
}
250261

251262
val experimental = (m.annotations.find { it is Experimental } as? Experimental)
@@ -266,6 +277,17 @@ class EMConfig {
266277
)
267278
}
268279

280+
private fun getValidAndExperimentalEnumValues(enumClass: Class<*>) : Pair<String,String>{
281+
assert(enumClass.isEnum)
282+
val elements = enumClass.getDeclaredMethod("values")
283+
.invoke(null) as Array<*>
284+
val experimentElements = elements.filter { it is WithExperimentalOptions && it.isExperimental() }
285+
val validElements = elements.filter { it !is WithExperimentalOptions || !it.isExperimental() }
286+
287+
val experimentalValues = experimentElements.joinToString(", ")
288+
val validValues = validElements.joinToString(", ")
289+
return Pair(validValues,experimentalValues)
290+
}
269291

270292
fun getConfigurationProperties(): List<KMutableProperty<*>> {
271293
return EMConfig::class.members
@@ -452,39 +474,71 @@ class EMConfig {
452474

453475
private fun updateValue(optionValue: String, m: KMutableProperty<*>) {
454476

455-
val returnType = m.returnType.javaType as Class<*>
477+
val returnType = m.returnType.javaType
456478

457-
/*
479+
if(returnType is Class<*>) {
480+
/*
458481
TODO: ugly checks. But not sure yet if can be made better in Kotlin.
459482
Could be improved with isSubtypeOf from 1.1?
460483
http://stackoverflow.com/questions/41553647/kotlin-isassignablefrom-and-reflection-type-checks
461484
*/
462-
try {
463-
if (Integer.TYPE.isAssignableFrom(returnType)) {
464-
m.setter.call(this, Integer.parseInt(optionValue))
485+
try {
486+
if (Integer.TYPE.isAssignableFrom(returnType)) {
487+
m.setter.call(this, Integer.parseInt(optionValue))
465488

466-
} else if (java.lang.Long.TYPE.isAssignableFrom(returnType)) {
467-
m.setter.call(this, java.lang.Long.parseLong(optionValue))
489+
} else if (java.lang.Long.TYPE.isAssignableFrom(returnType)) {
490+
m.setter.call(this, java.lang.Long.parseLong(optionValue))
468491

469-
} else if (java.lang.Double.TYPE.isAssignableFrom(returnType)) {
470-
m.setter.call(this, java.lang.Double.parseDouble(optionValue))
492+
} else if (java.lang.Double.TYPE.isAssignableFrom(returnType)) {
493+
m.setter.call(this, java.lang.Double.parseDouble(optionValue))
471494

472-
} else if (java.lang.Boolean.TYPE.isAssignableFrom(returnType)) {
473-
m.setter.call(this, parseBooleanStrict(optionValue))
495+
} else if (java.lang.Boolean.TYPE.isAssignableFrom(returnType)) {
496+
m.setter.call(this, parseBooleanStrict(optionValue))
474497

475-
} else if (java.lang.String::class.java.isAssignableFrom(returnType)) {
476-
m.setter.call(this, optionValue)
498+
} else if (java.lang.String::class.java.isAssignableFrom(returnType)) {
499+
m.setter.call(this, optionValue)
477500

478-
} else if (returnType.isEnum) {
479-
val valueOfMethod = returnType.getDeclaredMethod("valueOf",
480-
java.lang.String::class.java)
481-
m.setter.call(this, valueOfMethod.invoke(null, optionValue))
501+
} else if (returnType.isEnum) {
502+
val valueOfMethod = returnType.getDeclaredMethod("valueOf", java.lang.String::class.java)
503+
m.setter.call(this, valueOfMethod.invoke(null, optionValue))
482504

483-
} else {
484-
throw IllegalStateException("BUG: cannot handle type $returnType")
505+
} else {
506+
throw IllegalStateException("BUG: cannot handle type $returnType")
507+
}
508+
} catch (e: Exception) {
509+
throw ConfigProblemException("Failed to handle property '${m.name}': ${e.message}")
485510
}
486-
} catch (e: Exception) {
487-
throw ConfigProblemException("Failed to handle property '${m.name}': ${e.message}")
511+
} else if(returnType is ParameterizedType) {
512+
513+
if (!Collection::class.java.isAssignableFrom(returnType.rawType as Class<*>)) {
514+
throw IllegalStateException("Configuration '${m.name}' is parameterized, but not a collection")
515+
}
516+
if (returnType.actualTypeArguments.size != 1) {
517+
throw IllegalStateException("Configuration '${m.name}' has more than 1 generic type in a collection")
518+
}
519+
val generic = returnType.actualTypeArguments[0] as Class<*>
520+
if(!generic.isEnum){
521+
throw IllegalStateException("Content for configuration '${m.name}' is not an enumeration: ${generic.name}")
522+
}
523+
524+
val valueOfMethod = generic.getDeclaredMethod("valueOf", java.lang.String::class.java)
525+
526+
val collection = optionValue.split(",")
527+
.map {
528+
try {
529+
valueOfMethod.invoke(null, it)
530+
}catch (e: Exception){
531+
throw ConfigProblemException("Failed to handle property '${m.name}': ${e.message}")
532+
}
533+
}
534+
.run {
535+
if(Set::class.java.isAssignableFrom(returnType.rawType as Class<*>)){
536+
toSet()
537+
} else {
538+
this
539+
}
540+
}
541+
m.setter.call(this, collection)
488542
}
489543
}
490544

@@ -964,6 +1018,7 @@ class EMConfig {
9641018

9651019
val properties = getConfigurationProperties()
9661020
.filter { it.annotations.find { it is Experimental } != null }
1021+
.filter{ it.returnType.javaType is Class<*>} // TODO handle Lists of Enum
9671022
.filter {
9681023
val returnType = it.returnType.javaType as Class<*>
9691024
when {
@@ -975,6 +1030,7 @@ class EMConfig {
9751030
.map { it.name }
9761031

9771032
val enums = getConfigurationProperties()
1033+
.filter{ it.returnType.javaType is Class<*>} // TODO handle Lists of Enum
9781034
.filter {
9791035
val returnType = it.returnType.javaType as Class<*>
9801036
if (returnType.isEnum) {
@@ -1556,7 +1612,7 @@ class EMConfig {
15561612
@Cfg("Models used to learn input constraints and predict the response status before issuing a request. " +
15571613
"Supports both single-model and ensemble configurations. " +
15581614
"Ensemble model is a combination of a comma-separated list, e.g., GLM,NN,KDE.")
1559-
var aiModelForResponseClassification: String = "NONE"
1615+
var aiModelForResponseClassification: Set<AIResponseClassifierModel> = setOf(AIResponseClassifierModel.NONE)
15601616

15611617
@Experimental
15621618
@Cfg("Learning rate controlling the step size during parameter updates in classifiers. " +
@@ -3387,8 +3443,7 @@ class EMConfig {
33873443

33883444
// Sets the AI response classification models programmatically.
33893445
fun setAIModels(vararg models: AIResponseClassifierModel) {
3390-
aiModelForResponseClassification =
3391-
models.joinToString(",") { it.name }
3446+
aiModelForResponseClassification = models.toSet()
33923447
}
33933448

33943449
/**
@@ -3399,17 +3454,7 @@ class EMConfig {
33993454
*/
34003455
fun getAIModelForResponseClassification(): List<AIResponseClassifierModel> {
34013456
val models = aiModelForResponseClassification
3402-
.split(",")
3403-
.map { it.trim() }
3404-
.filter { it.isNotEmpty() }
3405-
.map {
3406-
try {
3407-
AIResponseClassifierModel.valueOf(it)
3408-
} catch (e: Exception) {
3409-
throw ConfigProblemException("Invalid AI model: $it")
3410-
}
3411-
}
3412-
.distinct()
3457+
.toList()
34133458
.sorted()
34143459

34153460
// EvoMaster accept NONE or a combination of the AI models and not both

core/src/main/kotlin/org/evomaster/core/docs/ConfigToMarkdown.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package org.evomaster.core.docs
33
import org.evomaster.core.EMConfig
44
import org.evomaster.core.utils.StringUtils
55
import java.io.File
6+
import java.lang.reflect.ParameterizedType
67
import java.nio.charset.Charset
78
import kotlin.reflect.KMutableProperty
89
import kotlin.reflect.jvm.javaType
@@ -151,11 +152,15 @@ object ConfigToMarkdown {
151152
if(default.isBlank()){
152153
default = "\"\""
153154
}
154-
val type = (opt.returnType.javaType as Class<*>)
155-
val typeName = if(type.isEnum){
156-
"Enum"
155+
val type = opt.returnType.javaType
156+
val typeName = if(type is Class<*>) {
157+
if (type.isEnum) {
158+
"Enum"
159+
} else {
160+
StringUtils.capitalization(type.simpleName)
161+
}
157162
} else {
158-
StringUtils.capitalization(type.simpleName)
163+
StringUtils.capitalization(((type as ParameterizedType).rawType as Class<*>).simpleName)
159164
}
160165

161166
val description = EMConfig.getDescription(opt)

core/src/main/kotlin/org/evomaster/core/search/service/Statistics.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ class Statistics : SearchListener {
494494

495495
return aiMetricsAsPairs(
496496
enabled = true,
497-
type = config.aiModelForResponseClassification,
497+
type = config.aiModelForResponseClassification.joinToString(","),
498498
accuracy = metrics.accuracy,
499499
precision = metrics.precision400,
500500
sensitivity = metrics.sensitivity400,

core/src/test/kotlin/org/evomaster/core/EMConfigTest.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.evomaster.core
22

33
import org.evomaster.client.java.controller.api.ControllerConstants
4+
import org.evomaster.core.EMConfig.AIResponseClassifierModel
45
import org.evomaster.core.config.ConfigProblemException
56
import org.evomaster.core.output.OutputFormat
67
import org.evomaster.core.output.naming.NamingStrategy
@@ -21,6 +22,27 @@ internal class EMConfigTest{
2122
private val blackBox = "blackBox"
2223
private val generateMongoData = "generateMongoData"
2324

25+
26+
@Test
27+
fun testChangeSetEnum(){
28+
29+
val parser = EMConfig.getOptionParser()
30+
31+
val opt = parser.recognizedOptions()["aiModelForResponseClassification"] ?:
32+
throw Exception("Cannot find option")
33+
34+
val x = "${AIResponseClassifierModel.DETERMINISTIC},${AIResponseClassifierModel.GAUSSIAN}"
35+
val options = parser.parse("--aiModelForResponseClassification", x)
36+
37+
assertEquals(x, opt.value(options))
38+
39+
val config = EMConfig()
40+
assertTrue("" + config.aiModelForResponseClassification.map { it.name }.sorted().joinToString(",") != x)
41+
42+
config.updateProperties(options)
43+
assertEquals(x, "" + config.aiModelForResponseClassification.map { it.name }.sorted().joinToString(","))
44+
}
45+
2446
@Test
2547
fun testDependsOnTrue(){
2648

docs/options.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ There are 3 types of options:
260260
|`abstractInitializationGeneToMutate`| __Boolean__. During mutation, whether to abstract genes for repeated SQL actions. *Default value*: `false`.|
261261
|`aiClassifierRepairActivation`| __Enum__. Specify how the classification of actions's response will be used to execute a possible repair on the action. *Valid values*: `PROBABILITY, THRESHOLD`. *Default value*: `THRESHOLD`.|
262262
|`aiEncoderType`| __Enum__. The encoding strategy applied to transform raw data to the encoded version. *Valid values*: `RAW, NORMAL, UNIT_NORMAL`. *Default value*: `NORMAL`.|
263-
|`aiModelForResponseClassification`| __String__. Models used to learn input constraints and predict the response status before issuing a request. Supports both single-model and ensemble configurations. Ensemble model is a combination of a comma-separated list, e.g., GLM,NN,KDE. *Default value*: `NONE`.|
263+
|`aiModelForResponseClassification`| __Set__. Models used to learn input constraints and predict the response status before issuing a request. Supports both single-model and ensemble configurations. Ensemble model is a combination of a comma-separated list, e.g., GLM,NN,KDE. *Valid values*: `NONE, GAUSSIAN, KDE, KNN, NN, GLM, DETERMINISTIC`. *Default value*: `[NONE]`.|
264264
|`aiResponseClassifierLearningRate`| __Double__. Learning rate controlling the step size during parameter updates in classifiers. Relevant for gradient-based models such as GLM and neural networks. A smaller value ensures stable but slower convergence, while a larger value speeds up training but may cause instability. *Default value*: `0.01`.|
265265
|`aiResponseClassifierMaxStoredSamples`| __Int__. Maximum number of stored samples for classifiers such as KNN and KDE models that rely on retaining encoded inputs. This value specifies the maximum number of samples stored for each endpoint. A higher value can improve classification accuracy by leveraging more historical data, but also increases memory usage. A lower value reduces memory consumption but may limit the classifier’s knowledge base. Typically, it is safe to keep this value between 10,000 and 50,000 when the encoded input vector is usually a list of doubles with a length under 20. Reservoir sampling is applied independently for each endpoint: if this maximum number is exceeded, new samples randomly replace existing ones, ensuring an unbiased selection of preserved data. As an example, for an API with 100 endpoints and an input vector of size 20, a maximum of 10,000 samples per endpoint would require roughly 200 MB of memory. *Default value*: `10000`.|
266266
|`aiResponseClassifierWarmup`| __Int__. Number of training iterations required to update classifier parameters. For example, in the Gaussian model this affects mean and variance updates. For neural network (NN) models, the warm-up should typically be larger than 1000. *Default value*: `100`.|

0 commit comments

Comments
 (0)