-
Notifications
You must be signed in to change notification settings - Fork 114
Http oracle timeout #1608
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Http oracle timeout #1608
Changes from all commits
89ec54a
e7cabb8
6b6353f
6c23581
e637561
c5a764a
f5c01e2
811208f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| package com.foo.rest.examples.bb.httptimeout | ||
|
|
||
| import org.evomaster.e2etests.utils.CoveredTargets | ||
| import org.springframework.boot.SpringApplication | ||
| import org.springframework.boot.autoconfigure.SpringBootApplication | ||
| import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration | ||
| import org.springframework.http.ResponseEntity | ||
| import org.springframework.web.bind.annotation.GetMapping | ||
| import org.springframework.web.bind.annotation.PathVariable | ||
| import org.springframework.web.bind.annotation.RequestMapping | ||
| import org.springframework.web.bind.annotation.RestController | ||
|
|
||
|
|
||
| @SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) | ||
| @RequestMapping(path = ["/api/timeout"]) | ||
| @RestController | ||
| open class BBHttpTimeoutApplication { | ||
|
|
||
| companion object { | ||
| @JvmStatic | ||
| fun main(args: Array<String>) { | ||
| SpringApplication.run(BBHttpTimeoutApplication::class.java, *args) | ||
| } | ||
| } | ||
|
|
||
| // slow endpoint: blocks longer than the client timeout, triggering a HTTP_TIMEOUT fault. | ||
| // the target is covered as soon as the request is handled, before the client gives up. | ||
| @GetMapping(path = ["/slow/{id}"]) | ||
| open fun slow(@PathVariable("id") id: Int): ResponseEntity<String> { | ||
| CoveredTargets.cover("timeout") | ||
| val deadline = System.currentTimeMillis() + 10_000 | ||
| while (System.currentTimeMillis() < deadline) { | ||
| try { | ||
| Thread.sleep(deadline - System.currentTimeMillis()) | ||
| } catch (e: InterruptedException) { | ||
| // ignore and keep blocking | ||
| } | ||
| } | ||
| return ResponseEntity.status(200).body("$id") | ||
| } | ||
|
|
||
| // clean | ||
| @GetMapping(path = ["/fast/{id}"]) | ||
| open fun fast(@PathVariable("id") id: Int): ResponseEntity<String> { | ||
| return ResponseEntity.status(200).body("$id") | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package com.foo.rest.examples.bb.httptimeout | ||
|
|
||
| import com.foo.rest.examples.bb.SpringController | ||
|
|
||
| class BBHttpTimeoutController : SpringController(BBHttpTimeoutApplication::class.java) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| package org.evomaster.e2etests.spring.rest.bb.httptimeout | ||
|
|
||
| import com.foo.rest.examples.bb.httptimeout.BBHttpTimeoutController | ||
| import org.evomaster.core.output.OutputFormat | ||
| import org.evomaster.core.problem.enterprise.DetectedFaultUtils | ||
| import org.evomaster.core.problem.enterprise.ExperimentalFaultCategory | ||
| import org.evomaster.e2etests.spring.rest.bb.SpringTestBase | ||
| import org.evomaster.e2etests.utils.EnterpriseTestBase | ||
| import org.junit.jupiter.api.Assertions.assertTrue | ||
| import org.junit.jupiter.api.BeforeAll | ||
| import org.junit.jupiter.params.ParameterizedTest | ||
| import org.junit.jupiter.params.provider.EnumSource | ||
|
|
||
| class BBHttpTimeoutEMTest : SpringTestBase() { | ||
|
|
||
| companion object { | ||
|
|
||
| init { | ||
| EnterpriseTestBase.shouldApplyInstrumentation = false | ||
| } | ||
|
|
||
| @BeforeAll | ||
| @JvmStatic | ||
| fun init() { | ||
| initClass(BBHttpTimeoutController()) | ||
| } | ||
| } | ||
|
|
||
| @ParameterizedTest | ||
| @EnumSource | ||
| fun testBlackBoxOutput(outputFormat: OutputFormat) { | ||
|
|
||
| executeAndEvaluateBBTest( | ||
| outputFormat, | ||
| "bbhttptimeout", | ||
| 5, | ||
| 6, | ||
| "timeout" | ||
| ) { args: MutableList<String> -> | ||
|
|
||
| setOption(args, "useExperimentalOracles", "true") | ||
| setOption(args, "tcpTimeoutMs", "2000") | ||
| setOption(args, "httpOracles", "true") | ||
|
|
||
| val solution = initAndRun(args) | ||
|
|
||
| assertTrue(solution.individuals.size >= 1) | ||
|
|
||
| val timeoutFaults = DetectedFaultUtils.getDetectedFaults(solution) | ||
| .filter { it.category == ExperimentalFaultCategory.HTTP_TIMEOUT } | ||
|
|
||
| // fault on the slow path | ||
| assertTrue(timeoutFaults.any { it.operationId.contains("/api/timeout/slow/") }) | ||
| // no false positive on the fast path | ||
| assertTrue(timeoutFaults.none { it.operationId.contains("/api/timeout/fast/") }) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,8 @@ import org.evomaster.core.output.dto.DtoCall | |
| import org.evomaster.core.output.dto.GeneToDto | ||
| import org.evomaster.core.output.formatter.OutputFormatter | ||
| import org.evomaster.core.problem.enterprise.EnterpriseActionGroup | ||
| import org.evomaster.core.problem.enterprise.EnterpriseActionResult | ||
| import org.evomaster.core.problem.enterprise.ExperimentalFaultCategory | ||
| import org.evomaster.core.problem.externalservice.httpws.HttpExternalServiceAction | ||
| import org.evomaster.core.problem.httpws.HttpWsAction | ||
| import org.evomaster.core.problem.httpws.HttpWsCallResult | ||
|
|
@@ -60,6 +62,21 @@ abstract class HttpWsTestCaseWriter : ApiTestCaseWriter() { | |
| return !(result as HttpWsCallResult).getTimedout() | ||
| } | ||
|
|
||
| /** | ||
| * For a HTTP_TIMEOUT fault, the call is expected to fail, and the assertion is expressed | ||
| * directly on the call: | ||
| * - JS: await expect(...).rejects.toThrow() | ||
| * - Python: with self.assertRaises(Exception): ... | ||
| */ | ||
| protected fun hasTimeoutFault(res: ActionResult): Boolean { | ||
| return res is EnterpriseActionResult | ||
| && res.getFaults().any { it.category == ExperimentalFaultCategory.HTTP_TIMEOUT } | ||
| } | ||
|
|
||
| protected fun expectsRejection(res: ActionResult) = format.isJavaScript() && hasTimeoutFault(res) | ||
|
|
||
| protected fun expectsAssertRaises(res: ActionResult) = format.isPython() && hasTimeoutFault(res) | ||
|
|
||
| fun startRequest(lines: Lines){ | ||
| when { | ||
| format.isJavaOrKotlin() -> lines.append("given()") | ||
|
|
@@ -114,7 +131,8 @@ abstract class HttpWsTestCaseWriter : ApiTestCaseWriter() { | |
|
|
||
| when { | ||
| format.isJavaOrKotlin() -> lines.append("given()") | ||
| format.isJavaScript() -> lines.append("await superagent") | ||
| // for a call expected to reject, wrap it in await expect(...).rejects.toThrow() | ||
| format.isJavaScript() -> lines.append(if (expectsRejection(res)) "await expect(superagent" else "await superagent") | ||
| format.isCsharp() -> lines.append("await Client") | ||
| format.isPython() -> lines.append("requests \\") | ||
| } | ||
|
|
@@ -398,7 +416,7 @@ abstract class HttpWsTestCaseWriter : ApiTestCaseWriter() { | |
| timeStartName = handleExecutionTimePrologue(lines); | ||
| } | ||
|
|
||
| if (res.invalidCall()) { | ||
| if (res.invalidCall() && !expectsRejection(res) && !expectsAssertRaises(res)) { | ||
| addActionInTryCatch(call, index, testCaseName, lines, res, testSuitePath, baseUrlOfSut) | ||
| } else { | ||
| addActionLines(call, index, testCaseName, lines, res, testSuitePath, baseUrlOfSut) | ||
|
|
@@ -502,6 +520,12 @@ abstract class HttpWsTestCaseWriter : ApiTestCaseWriter() { | |
| dtoVar = writeDto(call, lines) | ||
| } | ||
|
|
||
| val pyAssertRaises = expectsAssertRaises(res) | ||
| if (pyAssertRaises) { | ||
| lines.add("with self.assertRaises(Exception):") | ||
| lines.indent() | ||
| } | ||
|
|
||
| handleFirstLine(call, lines, res, responseVariableName) | ||
|
|
||
| when { | ||
|
|
@@ -516,6 +540,8 @@ abstract class HttpWsTestCaseWriter : ApiTestCaseWriter() { | |
| lines.indent(2) | ||
| //in SuperAgent, verb must be first | ||
| handleVerbEndpoint(baseUrlOfSut, call, lines) | ||
| //client timeout, same source as fuzzing tcpTimeoutMs | ||
| lines.add(".timeout({response: EM_HTTP_TIMEOUT_MS, deadline: EM_HTTP_TIMEOUT_MS})") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this |
||
| lines.append(getAcceptHeader(call, res)) | ||
| handleHeaders(call, lines) | ||
| handleBody(call, lines) | ||
|
|
@@ -537,6 +563,10 @@ abstract class HttpWsTestCaseWriter : ApiTestCaseWriter() { | |
| handleResponseDirectlyInTheCall(call, res, lines) | ||
| } | ||
| handleLastLine(call, res, lines, responseVariableName) | ||
|
|
||
| if (pyAssertRaises) { | ||
| lines.deindent() | ||
| } | ||
| return responseVariableName | ||
| } | ||
|
|
||
|
|
@@ -980,6 +1010,10 @@ abstract class HttpWsTestCaseWriter : ApiTestCaseWriter() { | |
| so, here we make it passes as long as a status was present | ||
| */ | ||
| lines.add(".ok(res => res.status)") | ||
| if (expectsRejection(res)) { | ||
| //close the await expect(...) and assert the call rejected | ||
| lines.append(").rejects.toThrow()") | ||
| } | ||
| } | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ import org.evomaster.core.output.TestWriterUtils | |
| import org.evomaster.core.output.TestWriterUtils.getWireMockVariableName | ||
| import org.evomaster.core.problem.enterprise.EnterpriseActionResult | ||
| import org.evomaster.core.problem.enterprise.EnterpriseIndividual | ||
| import org.evomaster.core.problem.enterprise.ExperimentalFaultCategory | ||
| import org.evomaster.core.problem.externalservice.HostnameResolutionAction | ||
| import org.evomaster.core.problem.externalservice.httpws.HttpExternalServiceAction | ||
| import org.evomaster.core.problem.externalservice.httpws.param.HttpWsResponseParam | ||
|
|
@@ -44,6 +45,10 @@ abstract class TestCaseWriter { | |
|
|
||
| companion object { | ||
| private val log = LoggerFactory.getLogger(TestCaseWriter::class.java) | ||
|
|
||
| // message for the assertion that flags a missing expected timeout (Java/Kotlin/C#) | ||
| // JS uses await expect(...).rejects.toThrow() and Python uses with self.assertRaises(...) | ||
| private const val EXPECTED_TIMEOUT_MSG = "Expected a timeout" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use JavaDoc style |
||
| } | ||
|
|
||
|
|
||
|
|
@@ -320,10 +325,19 @@ abstract class TestCaseWriter { | |
| format.isPython() -> lines.add("try:") | ||
| } | ||
|
|
||
| // a HTTP_TIMEOUT fault means the call is expected to time out (client timeout == fuzzing | ||
| // tcpTimeoutMs). if no timeout exception is thrown, the fault did not reproduce -> fail | ||
| val timeoutFault = res is EnterpriseActionResult | ||
| && res.getFaults().any { it.category == ExperimentalFaultCategory.HTTP_TIMEOUT } | ||
|
|
||
| lines.indented { | ||
| addActionLines(call,index, testCaseName, lines, res, testSuitePath, baseUrlOfSut) | ||
|
|
||
| if (shouldFailIfExceptionNotThrown(res)) { | ||
| if (timeoutFault) { | ||
| // only Java/Kotlin/C# reach here; JS uses expect(...).rejects.toThrow() and | ||
| // Python uses with self.assertRaises(...), neither wrapped in this try/catch | ||
| lines.add("fail(\"$EXPECTED_TIMEOUT_MSG\");") | ||
| } else if (shouldFailIfExceptionNotThrown(res)) { | ||
| if (!format.isJavaScript()) { | ||
| /* | ||
| TODO need a way to do it for JS, see | ||
|
|
@@ -372,17 +386,16 @@ abstract class TestCaseWriter { | |
| format.isPython() -> lines.add("except Exception as e:") | ||
| } | ||
|
|
||
| res.getErrorMessage()?.let { | ||
| lines.indented { | ||
| lines.indented { | ||
| res.getErrorMessage()?.let { | ||
| lines.addSingleCommentLine("${it.replace('\n', ' ').replace('\r', ' ')}") | ||
| } | ||
| } | ||
|
|
||
| if (format.isPython()) { | ||
| lines.indented { | ||
| if (format.isPython()) { | ||
| lines.add("pass") | ||
| } | ||
| } else { | ||
| } | ||
|
|
||
| if (!format.isPython()) { | ||
| lines.add("}") | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -465,6 +465,7 @@ class TestSuiteWriter { | |
| addImport("io.restassured.RestAssured", lines) | ||
| addImport("io.restassured.RestAssured.given", lines, true) | ||
| addImport("io.restassured.response.ValidatableResponse", lines) | ||
| addImport("io.restassured.config.HttpClientConfig", lines) | ||
| } | ||
|
|
||
| if ((config.isEnabledExternalServiceMocking() && solution.needWireMockServers()) | ||
|
|
@@ -533,6 +534,8 @@ class TestSuiteWriter { | |
| if (format.isJavaScript()) { | ||
| lines.add("process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';") | ||
| lines.add("const superagent = require(\"superagent\");") | ||
| // HTTP client timeout (ms) | ||
| lines.add("const EM_HTTP_TIMEOUT_MS = ${config.tcpTimeoutMs};") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this name |
||
|
|
||
| val jsUtils = JsLoader::class.java.getResource("/$javascriptUtilsFilename").readText() | ||
| saveToDisk(jsUtils, Paths.get(config.outputFolder, javascriptUtilsFilename)) | ||
|
|
@@ -587,6 +590,8 @@ class TestSuiteWriter { | |
| } | ||
| } | ||
| lines.add("from $pythonUtilsFilenameNoExtension import *") | ||
| // HTTP client timeout (seconds) | ||
| lines.add("EM_HTTP_TIMEOUT = ${config.tcpTimeoutMs / 1000.0}") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shared constant |
||
| val pythonUtils = PyLoader::class.java.getResource("/$pythonUtilsFilename").readText() | ||
| saveToDisk(pythonUtils, Paths.get(config.outputFolder, pythonUtilsFilename)) | ||
| } | ||
|
|
@@ -840,9 +845,20 @@ class TestSuiteWriter { | |
| addStatement("RestAssured.urlEncodingEnabled = false", lines) | ||
| } | ||
|
|
||
| if (config.enableBasicAssertions && format.isJavaOrKotlin()) { | ||
| if (format.isJavaOrKotlin()) { | ||
| // global HTTP client config. The socket timeout MUST match the one used during | ||
| // fuzzing (tcpTimeoutMs), so that timeout faults are reproduced consistently | ||
| lines.add("RestAssured.config = RestAssured.config()") | ||
| lines.indented { | ||
| if (config.enableBasicAssertions) { | ||
| lines.add(".jsonConfig(JsonConfig.jsonConfig().numberReturnType(JsonPathConfig.NumberReturnType.DOUBLE))") | ||
| lines.add(".redirect(redirectConfig().followRedirects(false))") | ||
| } | ||
| lines.add(".httpClient(HttpClientConfig.httpClientConfig()") | ||
| lines.indented { | ||
| lines.add(".setParam(\"http.socket.timeout\", ${config.tcpTimeoutMs})") | ||
| lines.add(".setParam(\"http.connection.timeout\", ${config.tcpTimeoutMs}))") | ||
| } | ||
| lines.add(".jsonConfig(JsonConfig.jsonConfig().numberReturnType(JsonPathConfig.NumberReturnType.DOUBLE))") | ||
| lines.add(".redirect(redirectConfig().followRedirects(false))") | ||
| lines.add(".encoderConfig(EncoderConfig.encoderConfig().encodeContentTypeAs(\"application/octet-stream\", ContentType.TEXT))") | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1312,6 +1312,32 @@ abstract class AbstractRestFitness : HttpWsFitness<RestIndividual>() { | |
| analyzeHttpSemantics(individual, actionResults, fv) | ||
| } | ||
|
|
||
| if(config.blackBox && config.isEnabledFaultCategory(ExperimentalFaultCategory.HTTP_TIMEOUT)){ | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add comment specifying why we are skipping for white-box testing |
||
| handleTimeout(individual, actionResults, fv) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * A timeout is treated as a fault: the SUT should rather answer quickly (eg 202 with a | ||
| * Location header for long computations) instead of hanging until the client gives up. | ||
| */ | ||
| private fun handleTimeout( | ||
| individual: RestIndividual, | ||
| actionResults: List<ActionResult>, | ||
| fv: FitnessValue | ||
| ) { | ||
| val actions = individual.seeMainExecutableActions() | ||
|
|
||
| for (index in actions.indices) { | ||
| val a = actions[index] | ||
| val r = actionResults.find { it.sourceLocalId == a.getLocalId() } as RestCallResult? ?: continue | ||
| if (!r.getTimedout()) continue | ||
|
|
||
| val category = ExperimentalFaultCategory.HTTP_TIMEOUT | ||
| val scenarioId = idMapper.handleLocalTarget(idMapper.getFaultDescriptiveId(category, a.getName())) | ||
| fv.updateTarget(scenarioId, 1.0, index) | ||
| r.addFault(DetectedFault(category, a.getName(), null)) | ||
| } | ||
| } | ||
|
|
||
| private fun analyzeHttpSemantics(individual: RestIndividual, actionResults: List<ActionResult>, fv: FitnessValue) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't this test fail because by default we have
httpOraclesbeingfalse?