Skip to content

Commit 1baddbe

Browse files
authored
Merge pull request #1616 from WebFuzzing/fix-bugs-in-assertions
Fix bugs in assertions
2 parents b7fdec9 + 3693174 commit 1baddbe

2 files changed

Lines changed: 112 additions & 11 deletions

File tree

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

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ abstract class ApiTestCaseWriter : TestCaseWriter() {
2424

2525
companion object{
2626
private val mapper = ObjectMapper()
27+
/*
28+
HTML entities may be decoded differently by clients/servers, making exact string assertions flaky.
29+
as this relates to asseration not sut, we fix it in the test generation instead of flakiness handling
30+
*/
31+
private val HTML_ENTITY_REGEX = Regex("&(?:#[0-9]+|#x[0-9a-fA-F]+|[A-Za-z][A-Za-z0-9]+);")
2732
}
2833

2934
protected fun createUniqueResponseVariableName(): String {
@@ -116,6 +121,10 @@ abstract class ApiTestCaseWriter : TestCaseWriter() {
116121
//TODO in the call above BODY was used... what's difference from TEXT?
117122
bodyIsString(bodyString, GeneUtils.EscapeMode.TEXT, bodyVarName)
118123
}
124+
// with bodyIsString, it may return null, then add this
125+
if (assertion == null) {
126+
return
127+
}
119128
if (flakyBodyString == null || flakyBodyString == bodyString) {
120129
lines.add(assertion)
121130
}else{
@@ -165,7 +174,7 @@ abstract class ApiTestCaseWriter : TestCaseWriter() {
165174
}
166175
}
167176
'"' -> {
168-
val isString = bodyIsString(bodyString, GeneUtils.EscapeMode.BODY, bodyVarName)
177+
val isString = bodyIsString(bodyString, GeneUtils.EscapeMode.BODY, bodyVarName) ?: return
169178
if (flakyBodyString == null || flakyBodyString == bodyString) {
170179
lines.add(isString)
171180
}else{
@@ -216,7 +225,7 @@ abstract class ApiTestCaseWriter : TestCaseWriter() {
216225
object and array.
217226
The rest is either ignored or leads to crash
218227
*/
219-
val value = bodyIsString(s,GeneUtils.EscapeMode.BODY, responseVariableName)
228+
val value = bodyIsString(s, GeneUtils.EscapeMode.BODY, responseVariableName) ?: return
220229

221230
val fs = flakyBodyString?.trim()
222231
if (fs == null || fs == s) {
@@ -248,7 +257,7 @@ abstract class ApiTestCaseWriter : TestCaseWriter() {
248257
payload (Java and Python don't seem to have such issue)
249258
*/
250259
// TODO flaky
251-
lines.add(bodyIsString(s, GeneUtils.EscapeMode.BODY, responseVariableName))
260+
bodyIsString(s, GeneUtils.EscapeMode.BODY, responseVariableName)?.let { lines.add(it) }
252261
}
253262
else -> throw IllegalStateException("Format not supported yet: $format")
254263
}
@@ -266,7 +275,8 @@ abstract class ApiTestCaseWriter : TestCaseWriter() {
266275
TODO should do check for when there are spaces in the field name
267276
TODO also need more tests to check all these edge cases
268277
*/
269-
format.isJavaOrKotlin() -> if (fieldPath.isEmpty()) "" else if (fieldPath.startsWith("'")) "$fieldPath." else "'$fieldPath'."
278+
// field path starts with [ is an array index, do not need additional quote
279+
format.isJavaOrKotlin() -> if (fieldPath.isEmpty()) "" else if (fieldPath.startsWith("'") || fieldPath.startsWith("[")) "$fieldPath." else "'$fieldPath'."
270280
format.isJavaScript() -> if (fieldPath.isEmpty()) "" else "${if (fieldPath.startsWith("[") || fieldPath.startsWith(".")) "" else "."}$fieldPath"
271281
format.isCsharp() -> if (fieldPath.isEmpty()) "" else "${if (fieldPath.startsWith("[")) "" else "."}$fieldPath"
272282
format.isPython() -> if (fieldPath.isEmpty()) "" else "${if (fieldPath.startsWith("[") || fieldPath.startsWith(".")) "" else "."}$fieldPath"
@@ -374,9 +384,13 @@ abstract class ApiTestCaseWriter : TestCaseWriter() {
374384
val left = when (value) {
375385
is Boolean -> "equalTo($value)"
376386
is Number -> "numberMatches(${handleNumberInJavaOrKotlinTest(value)})"
377-
is String -> "containsString(" +
378-
"\"${GeneUtils.applyEscapes(value as String, mode = GeneUtils.EscapeMode.ASSERTION, format = format)}" +
379-
"\")"
387+
is String -> {
388+
val content = GeneUtils.applyEscapes(value, mode = GeneUtils.EscapeMode.ASSERTION, format = format)
389+
val assertionContent = handleHtmlEntity(content) ?: return
390+
"containsString(" +
391+
"\"$assertionContent" +
392+
"\")"
393+
}
380394
else -> throw IllegalStateException("Unsupported type: ${value::class}")
381395
}
382396
if (isSuitableToPrint(left)) {
@@ -491,6 +505,9 @@ abstract class ApiTestCaseWriter : TestCaseWriter() {
491505
val items = (list as List<String>).joinToString {
492506
"\"${GeneUtils.applyEscapes(it, mode = GeneUtils.EscapeMode.ASSERTION, format = format)}\""
493507
}
508+
if (!isSuitableToPrint(items)) {
509+
return
510+
}
494511

495512
if (flakyList != null && (!flakyList.containsAll(list) || flakyList.size != list.size)) {
496513

@@ -590,16 +607,18 @@ abstract class ApiTestCaseWriter : TestCaseWriter() {
590607
return instruction
591608
}
592609

593-
protected fun bodyIsString(bodyString: String, mode: GeneUtils.EscapeMode, responseVariableName: String?): String {
610+
protected fun bodyIsString(bodyString: String, mode: GeneUtils.EscapeMode, responseVariableName: String?): String? {
594611

595-
val content = GeneUtils.applyEscapes(bodyString, mode, format = format)
612+
val originalContent = GeneUtils.applyEscapes(bodyString, mode, format = format)
613+
val content = handleHtmlEntity(originalContent) ?: return null
596614

597615
if (format.isJavaOrKotlin()) {
598616
return ".body(containsString(\"$content\"))"
599617
}
600618

601619
if (format.isJavaScript()) {
602-
return "expect($responseVariableName.text).toBe(\"$content\");"
620+
return "expect($responseVariableName.text).toContain(\"$content\");"
621+
603622
}
604623

605624
if (format.isCsharp()) {
@@ -608,7 +627,8 @@ abstract class ApiTestCaseWriter : TestCaseWriter() {
608627
content.startsWith("\\\"") -> content.substring(2, content.length - 2)
609628
else -> content
610629
}
611-
return "Assert.True($responseVariableName == \"$k\");"
630+
return "Assert.True($responseVariableName.Contains(\"$k\"));"
631+
612632
}
613633

614634
if (format.isPython()) {
@@ -618,6 +638,22 @@ abstract class ApiTestCaseWriter : TestCaseWriter() {
618638
throw IllegalStateException("Not supported format $format")
619639
}
620640

641+
/**
642+
* handle assertion text that may contain HTML entities.
643+
* If none are found, the original text is returned.
644+
* Otherwise, returns the first non-empty text segment.
645+
* Returns null if no non-empty segment can be extracted.
646+
*/
647+
private fun handleHtmlEntity(content: String): String? {
648+
if (!HTML_ENTITY_REGEX.containsMatchIn(content)) {
649+
return content
650+
}
651+
652+
return HTML_ENTITY_REGEX.split(content)
653+
.map { it.trim() }
654+
.firstOrNull { it.isNotEmpty() }
655+
}
656+
621657

622658
/**
623659
* Some fields might lead to flackiness, eg assertions on timestamps.
@@ -646,6 +682,7 @@ abstract class ApiTestCaseWriter : TestCaseWriter() {
646682
return (
647683
printableContent != "null" //TODO not so sure about this one... need to double-check
648684
&& !printableContent.contains("logged")
685+
&& !HTML_ENTITY_REGEX.containsMatchIn(printableContent)
649686
// is this for IP host:port addresses?
650687
&& !printableContent.contains("""\w+:\d{4,5}""".toRegex()))
651688
}

core/src/test/kotlin/org/evomaster/core/output/TestCaseWriterTest.kt

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,41 @@ class TestCaseWriterTest : WriterTestBase(){
106106
}
107107

108108

109+
@Test
110+
fun testTextAssertionWithHtmlEntityUsesStableFragment() {
111+
val format = OutputFormat.JAVA_JUNIT_4
112+
val writer = RestTestCaseWriter(getConfig(format), PartialOracles())
113+
val lines = Lines(format)
114+
115+
writer.handleTextPlainTextAssertion(
116+
"Unable to obtain a new access token for resource &#39;null&#39;.", // observed in the catwatch
117+
null,
118+
lines,
119+
null
120+
)
121+
122+
assertTrue(lines.toString().contains("containsString(\"Unable to obtain a new access token for resource\")"))
123+
assertFalse(lines.toString().contains("&#39"))
124+
}
125+
126+
@Test
127+
fun testJsonStringAssertionWithHtmlEntityUsesStableFragment() {
128+
val format = OutputFormat.JAVA_JUNIT_4
129+
val writer = RestTestCaseWriter(getConfig(format), PartialOracles())
130+
val lines = Lines(format)
131+
132+
writer.handleJsonStringAssertion(
133+
"{\"message\":\"Unable to obtain a new access token for resource &#39;null&#39;.\"}",
134+
null,
135+
lines,
136+
null,
137+
false
138+
)
139+
140+
assertTrue(lines.toString().contains("containsString(\"Unable to obtain a new access token for resource\")"))
141+
assertFalse(lines.toString().contains("&#39"))
142+
}
143+
109144

110145

111146
private fun buildEvaluatedIndividual(dbInitialization: MutableList<SqlAction>): Triple<OutputFormat, String, EvaluatedIndividual<RestIndividual>> {
@@ -1283,6 +1318,35 @@ public void test() throws Exception {
12831318
}
12841319

12851320

1321+
@Test
1322+
fun testJavaObjectAssertionInArrayUsesGPathIndex() {
1323+
val fooAction = RestCallAction("1", HttpVerb.GET, RestPath("/foo"), mutableListOf())
1324+
1325+
val (format, baseUrlOfSut, ei) = buildResourceEvaluatedIndividual(
1326+
dbInitialization = mutableListOf(),
1327+
groups = mutableListOf(
1328+
(mutableListOf<SqlAction>() to mutableListOf(fooAction))
1329+
),
1330+
format = OutputFormat.JAVA_JUNIT_4
1331+
)
1332+
1333+
val fooResult = ei.seeResult(fooAction.getLocalId()) as RestCallResult
1334+
fooResult.setTimedout(false)
1335+
fooResult.setStatusCode(200)
1336+
fooResult.setBody("[{}]") // example in restcountries
1337+
fooResult.setBodyType(MediaType.APPLICATION_JSON_TYPE)
1338+
1339+
val config = getConfig(format)
1340+
val test = TestCase(test = ei, name = "test")
1341+
1342+
val writer = RestTestCaseWriter(config, PartialOracles())
1343+
val lines = writer.convertToCompilableTestCode(test, baseUrlOfSut)
1344+
1345+
assertTrue(lines.toString().contains(".body(\"[0].isEmpty()\", is(true))"))
1346+
assertFalse(lines.toString().contains(".body(\"'[0]'.isEmpty()\", is(true))"))
1347+
}
1348+
1349+
12861350
@Test
12871351
fun testTestWithObjectAssertion(){
12881352
val fooAction = RestCallAction("1", HttpVerb.GET, RestPath("/foo"), mutableListOf())

0 commit comments

Comments
 (0)