Skip to content

Commit 23d37e3

Browse files
committed
improved handling of ids in chained calls
1 parent 2c88d51 commit 23d37e3

8 files changed

Lines changed: 196 additions & 61 deletions

File tree

core/src/main/kotlin/org/evomaster/core/output/service/RestTestCaseWriter.kt

Lines changed: 3 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -410,10 +410,6 @@ class RestTestCaseWriter : HttpWsTestCaseWriter {
410410

411411
//trying to infer linked ids from HTTP response
412412

413-
val extraTypeInfo = when {
414-
format.isKotlin() -> "<Object>"
415-
else -> ""
416-
}
417413
val baseUri: String = if (call.usePreviousLocationId != null) {
418414
/* A variable should NOT be enclosed by quotes */
419415
locationVar(call.usePreviousLocationId!!)
@@ -422,14 +418,9 @@ class RestTestCaseWriter : HttpWsTestCaseWriter {
422418
"\"${call.path.resolveOnlyPath(call.parameters)}\""
423419
}
424420

425-
//TODO code here should use same algorithm as in res.getResourceId()
426-
//TODO this is quite limited, would need proper refactoring
427-
val extract = when {
428-
format.isPython() -> "str($resVarName.json()['${res.getResourceIdName()}'])"
429-
format.isJavaScript() -> "$resVarName.body.${res.getResourceIdName()}"
430-
format.isJavaOrKotlin() -> "$resVarName.extract().body().path$extraTypeInfo(\"${res.getResourceIdName()}\").toString()"
431-
else -> throw IllegalStateException("Unhandled format: $format")
432-
}
421+
val idPointer = res.getResourceId()?.pointer ?: "/id"
422+
423+
val extract = extractValueFromJsonResponse(resVarName, idPointer)
433424

434425
when{
435426
format.isJavaScript() -> lines.add("const ")
@@ -443,31 +434,6 @@ class RestTestCaseWriter : HttpWsTestCaseWriter {
443434
}
444435
}
445436

446-
// private fun addDeclarationsForExpectations(lines: Lines, individual: EvaluatedIndividual<RestIndividual>) {
447-
// if (!partialOracles.generatesExpectation(individual)) {
448-
// return
449-
// }
450-
//
451-
// if (!format.isJavaOrKotlin()) {
452-
// //TODO will need to see if going to support JS and C# as well
453-
// return
454-
// }
455-
//
456-
// lines.addEmpty()
457-
// when {
458-
// format.isJava() -> lines.append("ExpectationHandler expectationHandler = expectationHandler()")
459-
// format.isKotlin() -> lines.append("val expectationHandler: ExpectationHandler = expectationHandler()")
460-
// }
461-
// lines.appendSemicolon()
462-
// }
463-
//
464-
// private fun handleExpectationSpecificLines(call: RestCallAction, lines: Lines, res: RestCallResult, name: String) {
465-
// lines.addEmpty()
466-
// if (partialOracles.generatesExpectation(call, res)) {
467-
// partialOracles.addExpectations(call, lines, res, name, format)
468-
// }
469-
// }
470-
471437
override fun addTestCommentBlock(lines: Lines, test: TestCase) {
472438

473439
val ind = test.test
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package org.evomaster.core.problem.rest
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException
4+
import com.fasterxml.jackson.databind.JsonMappingException
5+
import com.fasterxml.jackson.databind.JsonNode
6+
import com.fasterxml.jackson.databind.ObjectMapper
7+
import com.fasterxml.jackson.databind.node.JsonNodeType
8+
9+
10+
11+
object IdHeuristics {
12+
13+
private val mapper = ObjectMapper()
14+
15+
/**
16+
* Heuristically try to tell if the given string name is representing an id.
17+
* Note: we can never be 100% sure, so this is just a heuristic.
18+
*/
19+
fun heuristicIsId(s: String) = s.endsWith("id", true)
20+
21+
22+
fun getId(body: String): IdLocationValue? {
23+
24+
val node = try {
25+
mapper.readTree(body)
26+
} catch (e: JsonProcessingException) {
27+
return null
28+
} catch (e: JsonMappingException) {
29+
return null
30+
}
31+
if (node == null) {
32+
return null
33+
}
34+
35+
return findIdInNode(node, "/")
36+
}
37+
38+
private fun findIdInNode(node: JsonNode, path: String): IdLocationValue? {
39+
when (node.nodeType) {
40+
JsonNodeType.OBJECT, JsonNodeType.POJO -> {
41+
val id = node.fields().asSequence().firstOrNull { it.key.equals("id", true) }
42+
?: run {
43+
val candidates = node.fields().asSequence().filter {
44+
heuristicIsId(it.key) && it.value.isValueNode
45+
}.toList()
46+
if (candidates.isEmpty()) {
47+
null
48+
} else if (candidates.size == 1) {
49+
candidates.first()
50+
} else {
51+
val underscore = candidates.firstOrNull { it.key.endsWith("_id", true) }
52+
val capital = candidates.firstOrNull { it.key.endsWith("Id", false) }
53+
if (underscore != null) {
54+
underscore
55+
} else if (capital != null) {
56+
capital
57+
} else {
58+
null
59+
}
60+
}
61+
}
62+
63+
if (id == null) {
64+
/*
65+
special case of wrapped responses
66+
*/
67+
val d = node.fields().asSequence().firstOrNull { it.key.equals("data") }
68+
if (d != null) {
69+
return findIdInNode(d.value, path + "data/")
70+
}
71+
return null
72+
}
73+
74+
return IdLocationValue(path + id.key, id.value.asText())
75+
}
76+
77+
JsonNodeType.ARRAY -> {
78+
//unsure we really need to handle arrays in this case
79+
return null
80+
}
81+
82+
JsonNodeType.NUMBER, JsonNodeType.STRING -> {
83+
/*
84+
Assume response is the id itself, but just if it is a single word
85+
*/
86+
return IdLocationValue(path, node.asText())
87+
}
88+
89+
else -> {
90+
return null
91+
}
92+
}
93+
}
94+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.evomaster.core.problem.rest
2+
3+
class IdLocationValue(
4+
/**
5+
* RFC6901 JSON Pointer for location in JSON document
6+
*/
7+
val pointer: String,
8+
/**
9+
* Value in the node, read as text
10+
*/
11+
val value: String
12+
)

core/src/main/kotlin/org/evomaster/core/problem/rest/RestResponseFeeder.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,7 @@ object RestResponseFeeder {
2020

2121
private val stemmer = PorterStemmer()
2222

23-
/**
24-
* Heuristically try to tell if the given string name is representing an id.
25-
* Note: we can never be 100% sure, so this is just a heuristic.
26-
*/
27-
fun heuristicIsId(s: String) = s.endsWith("id", true)
23+
2824

2925

3026
/**
@@ -35,6 +31,11 @@ object RestResponseFeeder {
3531
*/
3632
fun handleResponse(source: RestCallAction, res: RestCallResult, pool: DataPool){
3733

34+
/*
35+
FIXME: some duplicate code with IdHeuristics, which is called directly in RestCallResult.
36+
TODO: need to re-check how stemmer was used, before doing the refactoring
37+
*/
38+
3839
val status = res.getStatusCode()
3940
if(status !in 200..299){
4041
//collect data only from successful requests

core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestCallResult.kt

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import com.google.common.annotations.VisibleForTesting
44
import com.google.gson.Gson
55
import com.google.gson.JsonObject
66
import org.evomaster.core.problem.httpws.HttpWsCallResult
7+
import org.evomaster.core.problem.rest.IdHeuristics
8+
import org.evomaster.core.problem.rest.IdLocationValue
79
import org.evomaster.core.search.action.Action
810
import javax.ws.rs.core.MediaType
911

@@ -24,9 +26,8 @@ class RestCallResult : HttpWsCallResult {
2426
return RestCallResult(this)
2527
}
2628

27-
fun getResourceIdName() = "id"
2829

29-
fun getResourceId(): String? {
30+
fun getResourceId(): IdLocationValue? {
3031

3132
/*
3233
TODO should use more sophisticated algorithm, taking into account what done in RestResponseFeeder
@@ -37,18 +38,8 @@ class RestCallResult : HttpWsCallResult {
3738
return null
3839
}
3940

40-
return getBody()?.let {
41-
try {
42-
/*
43-
TODO: "id" is the most common word, but could check
44-
if others are used as well.
45-
*/
46-
Gson().fromJson(it, JsonObject::class.java).get(getResourceIdName())?.asString
47-
} catch (e: Exception){
48-
//nothing to do
49-
null
50-
}
51-
}
41+
val body = getBody() ?: return null
42+
return IdHeuristics.getId(body)
5243
}
5344

5445

core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/AbstractRestFitness.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -934,7 +934,7 @@ abstract class AbstractRestFitness : HttpWsFitness<RestIndividual>() {
934934
val id = rcr.getResourceId()
935935

936936
if (id != null && builder.hasParameterChild(a)) {
937-
location = a.resolvedPath() + "/" + id
937+
location = a.resolvedPath() + "/" + id.value
938938
rcr.setHeuristicsForChainedLocation(true)
939939
}
940940
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package org.evomaster.core.search.service
33
import com.google.inject.Inject
44
import opennlp.tools.stemmer.PorterStemmer
55
import org.evomaster.core.EMConfig
6+
import org.evomaster.core.problem.rest.IdHeuristics
67
import org.evomaster.core.problem.rest.data.RestCallAction
78
import org.evomaster.core.problem.rest.RestResponseFeeder
89
import org.evomaster.core.problem.rest.param.PathParam
@@ -75,7 +76,7 @@ class DataPool() {
7576
val x = extractValue(gene.name, context)
7677
?: return false
7778

78-
if(RestResponseFeeder.heuristicIsId(gene.name) && gene.getFirstParent(PathParam::class.java)!=null){
79+
if(IdHeuristics.heuristicIsId(gene.name) && gene.getFirstParent(PathParam::class.java)!=null){
7980
val action = gene.getFirstParent(RestCallAction::class.java)
8081
if(action != null && action.verb.isWriteOperation()){
8182
/*

core/src/test/kotlin/org/evomaster/core/problem/rest/RestCallResultTest.kt

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package org.evomaster.core.problem.rest
22

33
import org.evomaster.core.problem.rest.data.RestCallResult
4-
import org.junit.Assert.assertEquals
4+
import org.junit.jupiter.api.Assertions.assertEquals
5+
import org.junit.jupiter.api.Assertions.assertNull
56
import org.junit.jupiter.api.Test
67
import javax.ws.rs.core.MediaType
78

@@ -13,7 +14,7 @@ internal class RestCallResultTest {
1314
rc.setBody("{\"id\":\"735\"}")
1415
rc.setBodyType(MediaType.APPLICATION_JSON_TYPE)
1516

16-
val res = rc.getResourceId()
17+
val res = rc.getResourceId()!!.value
1718

1819
assertEquals("735", res)
1920
}
@@ -23,8 +24,77 @@ internal class RestCallResultTest {
2324
rc.setBody("{\"id\":735}")
2425
rc.setBodyType(MediaType.APPLICATION_JSON_TYPE)
2526

26-
val res = rc.getResourceId()
27+
val res = rc.getResourceId()!!.value
2728

2829
assertEquals("735", res)
2930
}
31+
32+
private fun createCallResult(body: String) : RestCallResult {
33+
val rc = RestCallResult("",false)
34+
rc.setBody(body)
35+
rc.setBodyType(MediaType.APPLICATION_JSON_TYPE)
36+
return rc
37+
}
38+
39+
@Test
40+
fun testDisambiguation(){
41+
val rc = createCallResult("""
42+
{
43+
"solid": "a",
44+
"timid": "b",
45+
"fooId": "c",
46+
"void": "d"
47+
}
48+
""".trimIndent())
49+
50+
val res = rc.getResourceId()!!
51+
52+
assertEquals("c", res.value)
53+
assertEquals("/fooId", res.pointer)
54+
}
55+
56+
@Test
57+
fun testWrappedData(){
58+
val rc = createCallResult("""
59+
{
60+
"errors": null,
61+
"data": {
62+
"foo": "a",
63+
"placid": "b",
64+
"bar_id": "c"
65+
}
66+
}
67+
""".trimIndent())
68+
69+
val res = rc.getResourceId()!!
70+
71+
assertEquals("c", res.value)
72+
assertEquals("/data/bar_id", res.pointer)
73+
}
74+
75+
@Test
76+
fun testWrongWrapping(){
77+
val rc = createCallResult("""
78+
{
79+
"data": null
80+
"errors": {
81+
"id": "foo",
82+
"message": "an error",
83+
}
84+
}
85+
""".trimIndent())
86+
87+
val res = rc.getResourceId()
88+
assertNull(res)
89+
}
90+
91+
@Test
92+
fun testPrimitive(){
93+
val rc = createCallResult("42")
94+
95+
val res = rc.getResourceId()!!
96+
97+
assertEquals("42", res.value)
98+
assertEquals("/", res.pointer)
99+
}
30100
}

0 commit comments

Comments
 (0)