Skip to content

Commit 8e4c63d

Browse files
add final test for printing in xml, add resurceSchema aware builder and tests
1 parent 56ec4b3 commit 8e4c63d

4 files changed

Lines changed: 451 additions & 102 deletions

File tree

Lines changed: 108 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,165 @@
11
package org.evomaster.core.search.gene.builder
22

33
import org.evomaster.core.search.gene.Gene
4+
import org.evomaster.core.search.gene.ObjectGene
45
import org.evomaster.core.search.gene.collection.ArrayGene
56
import org.evomaster.core.search.gene.collection.EnumGene
67
import org.evomaster.core.search.gene.collection.PairGene
78
import org.evomaster.core.search.gene.jsonpatch.JsonPatchFromPathGene
89
import org.evomaster.core.search.gene.jsonpatch.JsonPatchOperationGene
910
import org.evomaster.core.search.gene.jsonpatch.JsonPatchPathOnlyGene
1011
import org.evomaster.core.search.gene.jsonpatch.JsonPatchPathValueGene
12+
import org.evomaster.core.search.gene.numeric.IntegerGene
13+
import org.evomaster.core.search.gene.placeholder.CycleObjectGene
1114
import org.evomaster.core.search.gene.string.StringGene
1215
import org.evomaster.core.search.gene.wrapper.ChoiceGene
16+
import org.evomaster.core.search.gene.wrapper.OptionalGene
17+
import org.evomaster.core.search.service.Randomness
1318

1419
/**
1520
* Builds a JSON Patch document gene (RFC 6902).
1621
*
17-
* [resourceSchema] is accepted but not yet analysed; schema-aware path extraction
18-
* will be wired in a future step. Until then, [DEFAULT_PATHS] are used as placeholders.
22+
* When [resourceSchema] is provided, paths and value gene types are derived from the schema.
23+
* Otherwise, random path strings are generated using [randomness] (or a fresh [Randomness]
24+
* if none is supplied), and both [StringGene] and [IntegerGene] are offered as value choices.
1925
*/
2026
object JsonPatchDocumentGeneBuilder {
2127

2228
const val MIN_SIZE = 1
2329
const val DEFAULT_MAX_SIZE = 10
30+
private const val RANDOM_PATH_COUNT = 4
2431

25-
val DEFAULT_PATHS = listOf("/", "/a", "/b", "/c")
32+
/** A single patchable field extracted from a resource schema. */
33+
internal data class SchemaField(val path: String, val gene: Gene)
34+
35+
/**
36+
* Walks [schema] recursively and returns one [SchemaField] per reachable leaf field,
37+
* using JSON Pointer notation for paths (e.g. "/name", "/address/street").
38+
*
39+
* - [ObjectGene]: descends into each fixed field.
40+
* - [OptionalGene]: unwraps and descends using the same path prefix.
41+
* - [CycleObjectGene]: skipped to avoid infinite loops.
42+
* - Everything else (leaf genes and [ArrayGene]): treated as a patchable target at [prefix].
43+
*/
44+
internal fun extractSchemaFields(schema: Gene, prefix: String = ""): List<SchemaField> {
45+
return when (schema) {
46+
is CycleObjectGene -> emptyList()
47+
is OptionalGene -> extractSchemaFields(schema.gene, prefix)
48+
is ObjectGene -> schema.fixedFields.flatMap { field ->
49+
extractSchemaFields(field, "$prefix/${field.name}")
50+
}
51+
else -> if (prefix.isNotEmpty()) listOf(SchemaField(prefix, schema.copy())) else emptyList()
52+
}
53+
}
2654

2755
/**
2856
* Builds the ArrayGene of patch operations.
2957
*
3058
* All six RFC 6902 operation choices (remove, move, copy, add, replace, test) are always
31-
* present in the ChoiceGene template so that mutation can switch freely between them.
32-
* [resourceSchema] is reserved for future schema-based path extraction and is ignored for now.
59+
* present in the ChoiceGene template.
60+
*
61+
* - If [resourceSchema] yields fields: paths and typed value genes come from the schema.
62+
* - Otherwise: paths are generated randomly via [randomness] (a fresh [Randomness] is used
63+
* when none is supplied), and value choices are [StringGene] + [IntegerGene].
3364
*/
3465
fun buildOperationsArray(
35-
// TODO: resourceSchema is ignored in this PR — path extraction will be wired in a follow-up
3666
resourceSchema: Gene? = null,
37-
paths: List<String> = DEFAULT_PATHS
67+
randomness: Randomness? = null
3868
): ArrayGene<ChoiceGene<JsonPatchOperationGene>> {
3969

40-
val effectivePaths = paths.ifEmpty { listOf("/") }
41-
val pathEnum = EnumGene<String>("path", effectivePaths)
70+
val schemaFields = resourceSchema?.let { extractSchemaFields(it) }.orEmpty()
4271

43-
val choices = mutableListOf<JsonPatchOperationGene>()
72+
return if (schemaFields.isNotEmpty()) {
73+
buildFromSchemaFields(schemaFields)
74+
} else {
75+
buildFromPaths(generateRandomPaths(randomness ?: Randomness()))
76+
}
77+
}
4478

45-
choices.add(JsonPatchPathOnlyGene(JsonPatchOperationGene.OP_REMOVE, JsonPatchOperationGene.OP_REMOVE, pathEnum.copy() as EnumGene<String>))
79+
// ---------------------------------------------------------------------------
80+
// private helpers
81+
// ---------------------------------------------------------------------------
82+
83+
private fun generateRandomPaths(randomness: Randomness): List<String> =
84+
generateSequence { "/${randomness.nextWordString(2, 6)}" }
85+
.take(RANDOM_PATH_COUNT * 2)
86+
.distinct()
87+
.take(RANDOM_PATH_COUNT)
88+
.toList()
89+
.ifEmpty { listOf("/field") }
90+
91+
private fun buildFromSchemaFields(
92+
fields: List<SchemaField>
93+
): ArrayGene<ChoiceGene<JsonPatchOperationGene>> {
94+
val allPaths = fields.map { it.path }
95+
val pathEnum = EnumGene<String>("path", allPaths)
96+
97+
val pathValueEntries: List<PairGene<EnumGene<String>, Gene>> =
98+
fields.groupBy { it.gene::class }
99+
.entries
100+
.mapIndexed { index, (_, group) ->
101+
PairGene(
102+
"entry_$index",
103+
EnumGene("path", group.map { it.path }),
104+
group.first().gene.copy()
105+
)
106+
}
107+
108+
return assemble(pathEnum, pathValueEntries)
109+
}
110+
111+
/** No-schema case: both [StringGene] and [IntegerGene] offered as value choices. */
112+
private fun buildFromPaths(
113+
paths: List<String>
114+
): ArrayGene<ChoiceGene<JsonPatchOperationGene>> {
115+
val pathEnum = EnumGene<String>("path", paths)
116+
val entries = listOf<PairGene<EnumGene<String>, Gene>>(
117+
PairGene("entry_string", pathEnum.copy() as EnumGene<String>, StringGene("value")),
118+
PairGene("entry_int", pathEnum.copy() as EnumGene<String>, IntegerGene("value"))
119+
)
120+
return assemble(pathEnum, entries)
121+
}
122+
123+
private fun assemble(
124+
pathEnum: EnumGene<String>,
125+
pathValueEntries: List<PairGene<EnumGene<String>, Gene>>
126+
): ArrayGene<ChoiceGene<JsonPatchOperationGene>> {
127+
128+
val choices = mutableListOf<JsonPatchOperationGene>()
46129

130+
choices.add(
131+
JsonPatchPathOnlyGene(
132+
JsonPatchOperationGene.OP_REMOVE, JsonPatchOperationGene.OP_REMOVE,
133+
pathEnum.copy() as EnumGene<String>
134+
)
135+
)
47136
choices.add(
48137
JsonPatchFromPathGene(
49-
JsonPatchOperationGene.OP_MOVE,
50-
JsonPatchOperationGene.OP_MOVE,
138+
JsonPatchOperationGene.OP_MOVE, JsonPatchOperationGene.OP_MOVE,
51139
fromGene = pathEnum.copy() as EnumGene<String>,
52140
pathGene = pathEnum.copy() as EnumGene<String>
53141
)
54142
)
55143
choices.add(
56144
JsonPatchFromPathGene(
57-
JsonPatchOperationGene.OP_COPY,
58-
JsonPatchOperationGene.OP_COPY,
145+
JsonPatchOperationGene.OP_COPY, JsonPatchOperationGene.OP_COPY,
59146
fromGene = pathEnum.copy() as EnumGene<String>,
60147
pathGene = pathEnum.copy() as EnumGene<String>
61148
)
62149
)
63150

64-
// TODO: replace StringGene with a schema-aware gene derived from resourceSchema
65-
// (e.g. IntegerGene, BooleanGene, ObjectGene) once path extraction is wired in
66-
val entryTemplate = PairGene<EnumGene<String>, Gene>(
67-
"entry_0",
68-
pathEnum.copy() as EnumGene<String>,
69-
StringGene("value")
70-
)
71-
72151
for (op in listOf(JsonPatchOperationGene.OP_ADD, JsonPatchOperationGene.OP_REPLACE, JsonPatchOperationGene.OP_TEST)) {
73152
choices.add(
74153
JsonPatchPathValueGene(
75-
op,
76-
op,
77-
ChoiceGene("${op}PathValue", listOf(entryTemplate.copy() as PairGene<EnumGene<String>, Gene>))
154+
op, op,
155+
ChoiceGene("${op}PathValue", pathValueEntries.map {
156+
it.copy() as PairGene<EnumGene<String>, Gene>
157+
})
78158
)
79159
)
80160
}
81161

82-
val template = ChoiceGene<JsonPatchOperationGene>("operation", choices)
83-
return ArrayGene("operations", template, minSize = MIN_SIZE, maxSize = DEFAULT_MAX_SIZE)
162+
return ArrayGene("operations", ChoiceGene("operation", choices),
163+
minSize = MIN_SIZE, maxSize = DEFAULT_MAX_SIZE)
84164
}
85165
}

core/src/main/kotlin/org/evomaster/core/search/gene/jsonpatch/JsonPatchDocumentGene.kt

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ class JsonPatchDocumentGene private constructor(
2929
operationsArr: ArrayGene<ChoiceGene<JsonPatchOperationGene>>
3030
) : CompositeFixedGene(name, listOf(operationsArr)) {
3131

32-
constructor(name: String, resourceSchema: Gene? = null)
33-
: this(name, resourceSchema, JsonPatchDocumentGeneBuilder.buildOperationsArray(resourceSchema))
32+
constructor(name: String, resourceSchema: Gene? = null, randomness: Randomness? = null)
33+
: this(name, resourceSchema, JsonPatchDocumentGeneBuilder.buildOperationsArray(resourceSchema, randomness = randomness))
3434

3535
companion object {
3636
val MIN_SIZE get() = JsonPatchDocumentGeneBuilder.MIN_SIZE
@@ -53,7 +53,10 @@ class JsonPatchDocumentGene private constructor(
5353
mode: GeneUtils.EscapeMode?,
5454
targetFormat: OutputFormat?,
5555
extraCheck: Boolean
56-
): String = operationsArray.getValueAsPrintableString(previousGenes, mode, targetFormat, extraCheck)
56+
): String {
57+
val inner = operationsArray.getValueAsPrintableString(previousGenes, mode, targetFormat, extraCheck)
58+
return if (mode == GeneUtils.EscapeMode.XML) "<patch>$inner</patch>" else inner
59+
}
5760

5861
override fun copyContent(): Gene =
5962
JsonPatchDocumentGene(
@@ -64,7 +67,16 @@ class JsonPatchDocumentGene private constructor(
6467

6568
override fun containsSameValueAs(other: Gene): Boolean {
6669
if (other !is JsonPatchDocumentGene) throw IllegalArgumentException("Invalid gene type ${other.javaClass}")
67-
return operationsArray.containsSameValueAs(other.operationsArray)
70+
val schemaMatch = when {
71+
resourceSchema == null && other.resourceSchema == null -> true
72+
resourceSchema == null || other.resourceSchema == null -> false
73+
else -> try {
74+
resourceSchema.containsSameValueAs(other.resourceSchema)
75+
} catch (_: IllegalArgumentException) {
76+
false
77+
}
78+
}
79+
return schemaMatch && operationsArray.containsSameValueAs(other.operationsArray)
6880
}
6981

7082
override fun unsafeCopyValueFrom(other: Gene): Boolean {

0 commit comments

Comments
 (0)