Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")

Copy link
Copy Markdown
Collaborator

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 httpOracles being false?

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
Expand Up @@ -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
Expand Down Expand Up @@ -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()")
Expand Down Expand Up @@ -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 \\")
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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})")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this EM_HTTP_TIMEOUT_MS is referring to a variable declared elsewhere? in such case, the name of the variable should be put in a shared constant

lines.append(getAcceptHeader(call, res))
handleHeaders(call, lines)
handleBody(call, lines)
Expand All @@ -537,6 +563,10 @@ abstract class HttpWsTestCaseWriter : ApiTestCaseWriter() {
handleResponseDirectlyInTheCall(call, res, lines)
}
handleLastLine(call, res, lines, responseVariableName)

if (pyAssertRaises) {
lines.deindent()
}
return responseVariableName
}

Expand Down Expand Up @@ -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()")
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,11 +303,12 @@ class RestTestCaseWriter : HttpWsTestCaseWriter {
if (bodyParam != null) {
lines.append(", data=body")
}
if(config.testTimeout > 0) {
if(config.tcpTimeoutMs > 0) {
/*
As timeout at test level does not work reliably in Python, we do timeout as well in each HTTP call.
Client timeout per HTTP call, same source as fuzzing tcpTimeoutMs.
Also, timeout at test level does not work reliably in Python.
*/
lines.append(", timeout=${config.testTimeout}")
lines.append(", timeout=EM_HTTP_TIMEOUT")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use JavaDoc style /** */ for comments on fields and methos

}


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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("}")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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};")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this name EM_HTTP_TIMEOUT_MS should be in a shared constant, as re-used in few places


val jsUtils = JsLoader::class.java.getResource("/$javascriptUtilsFilename").readText()
saveToDisk(jsUtils, Paths.get(config.outputFolder, javascriptUtilsFilename))
Expand Down Expand Up @@ -587,6 +590,8 @@ class TestSuiteWriter {
}
}
lines.add("from $pythonUtilsFilenameNoExtension import *")
// HTTP client timeout (seconds)
lines.add("EM_HTTP_TIMEOUT = ${config.tcpTimeoutMs / 1000.0}")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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))
}
Expand Down Expand Up @@ -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))")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ enum class ExperimentalFaultCategory(
"TODO"),
HTTP_INVALID_ALLOW(919, "Invalid allow", "invalidAllow",
"TODO"),
HTTP_TIMEOUT(921, "Timeout", "timeout",
"TODO"),

HTTP_STATUS_NO_NON_STANDARD_CODES(950, "HTTP/REST-Design Violation: no-non-standard-codes", "invalidStatusCode", "TODO"),
HTTP_STATUS_NO_201_IF_DELETE(951, "HTTP/REST-Design Violation: no-201-if-delete", "201OnDelete", "TODO"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ import org.evomaster.core.problem.rest.data.RestCallResult
import org.evomaster.core.problem.rest.data.RestIndividual
import org.evomaster.core.problem.rest.data.RestPath
import org.evomaster.core.problem.rest.param.PathParam
import org.evomaster.core.problem.rest.service.fitness.RestFitness
import org.evomaster.core.problem.rest.service.sampler.AbstractRestSampler
import org.evomaster.core.search.EvaluatedIndividual
import org.evomaster.core.search.Solution
import org.evomaster.core.search.service.Archive
import org.evomaster.core.search.service.FitnessFunction
import org.evomaster.core.search.service.IdMapper
import org.evomaster.core.search.service.Randomness
import org.evomaster.core.search.service.time.ExecutionPhaseController
Expand Down Expand Up @@ -48,7 +48,7 @@ class HttpSemanticsService : TimeBoxedPhase{
private lateinit var randomness: Randomness

@Inject
private lateinit var fitness: RestFitness
private lateinit var fitness: FitnessFunction<RestIndividual>

@Inject
private lateinit var idMapper: IdMapper
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1312,6 +1312,32 @@ abstract class AbstractRestFitness : HttpWsFitness<RestIndividual>() {
analyzeHttpSemantics(individual, actionResults, fv)
}

if(config.blackBox && config.isEnabledFaultCategory(ExperimentalFaultCategory.HTTP_TIMEOUT)){

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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) {
Expand Down
Loading
Loading