Skip to content

Commit

Permalink
Merge pull request #811 from EMResearch/issue-803
Browse files Browse the repository at this point in the history
enabling constraint handling + working on issue #803
  • Loading branch information
arcuri82 authored Sep 29, 2023
2 parents 94b9cd5 + 48a0b64 commit ef1d018
Show file tree
Hide file tree
Showing 12 changed files with 237 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,31 @@ public static boolean includesTaintInput(String value){
* Create a tainted value, with the input id being part of it
*/
public static String getTaintName(int id){
return getTaintName(id,0);
}

public static String getTaintName(int id, int minLength){
if(id < 0){
throw new IllegalArgumentException("Negative id");
}
if(minLength < 0){
throw new IllegalArgumentException("Negative minLength");
}
/*
Note: this is quite simple, we simply add a unique prefix
and postfix, in lowercase.
But we would not be able to check if the part of the id was
modified.
*/
return PREFIX + id + POSTFIX;

String s = "" + id;
String taint = PREFIX + s + POSTFIX;
if(taint.length() < minLength){
//need padding
int diff = minLength - taint.length();
taint = PREFIX + s + new String(new char[diff]).replace("\0", "0") + POSTFIX;
}
return taint;
}

/**
Expand All @@ -74,7 +89,7 @@ public static String getTaintName(int id){
* Not sure if there is really any simple workaround... but hopefully should be
* so rare that we can live with it
*/
public static int getTaintNameMaxLength(){
return PREFIX.length() + POSTFIX.length() + 6;
public static boolean doesTaintNameSatisfiesLengthConstraints(String id, int maxLength){
return (PREFIX.length() + POSTFIX.length() + id.length()) <= maxLength;
}
}
3 changes: 1 addition & 2 deletions core/src/main/kotlin/org/evomaster/core/EMConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1917,8 +1917,7 @@ class EMConfig {
var externalRequestResponseSelectionStrategy = ExternalRequestResponseSelectionStrategy.EXACT

@Cfg("Whether to employ constraints specified in API schema (e.g., OpenAPI) in test generation")
@Experimental
var enableSchemaConstraintHandling = false
var enableSchemaConstraintHandling = true

@Cfg("a probability of enabling single insertion strategy to insert rows into database.")
@Probability(activating = true)
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/kotlin/org/evomaster/core/StaticCounter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ class StaticCounter {

fun getAndIncrease() = counter++

fun get() = counter

fun reset() {
counter = 0
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import java.net.URI
import java.net.URISyntaxException
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
import kotlin.math.max

/**
* https://github.com/OAI/OpenAPI-Specification/blob/3.0.1/versions/3.0.1.md
Expand Down Expand Up @@ -366,7 +367,7 @@ object RestActionBuilderV3 {
would lead to 2 variables, or any other char that does affect the
structure of the URL, like '.'
*/
gene = StringGene(gene.name, gene.value, 1, gene.maxLength, listOf('/', '.'))
gene = StringGene(gene.name, gene.value, max(gene.minLength, 1), gene.maxLength, listOf('/', '.'))
}

if (p.required != true && p.`in` != "path" && gene !is OptionalGene) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import org.evomaster.core.search.service.mutator.genemutation.AdditionalGeneMuta
import org.evomaster.core.search.service.mutator.genemutation.SubsetGeneMutationSelectionStrategy
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.lang.IllegalStateException
import kotlin.math.max
import kotlin.math.min

class StringGene(
Expand Down Expand Up @@ -113,12 +115,12 @@ class StringGene(
get() {return children}


fun actualMaxLength() : Int {
private fun actualMaxLength() : Int {

val state = getSearchGlobalState()
?: return maxLength

return min(maxLength, state.config.maxLengthForStrings)
return max(minLength, min(maxLength, state.config.maxLengthForStrings))
}


Expand Down Expand Up @@ -177,7 +179,17 @@ class StringGene(
val maxForRandomization = getSearchGlobalState()?.config?.maxLengthForStringsAtSamplingTime ?: 16

val adjustedMin = minLength
val adjustedMax = min(maxLength, maxForRandomization)
var adjustedMax = min(maxLength, maxForRandomization)

if(adjustedMax < adjustedMin){
/*
this can happen if there are constraints on min length that are longer than our typical strings.
even if we do not want to use too long strings for performance reasons, we still must satisfy
any min constrains
*/
assert(minLength <= maxLength)
adjustedMax = adjustedMin
}

if(adjustedMax == 0 && adjustedMin == adjustedMax){
//only empty string is allowed
Expand Down Expand Up @@ -402,7 +414,7 @@ class StringGene(

fun redoTaint(apc: AdaptiveParameterControl, randomness: Randomness) : Boolean{

if(TaintInputName.getTaintNameMaxLength() > actualMaxLength()){
if(!TaintInputName.doesTaintNameSatisfiesLengthConstraints("${StaticCounter.get()}", actualMaxLength())){
return false
}

Expand Down Expand Up @@ -437,8 +449,16 @@ class StringGene(
return false
}

/**
* Force a tainted value. Must guarantee min-max length constraints are satisfied
*/
fun forceTaintedValue() {
value = TaintInputName.getTaintName(StaticCounter.getAndIncrease())
val taint = TaintInputName.getTaintName(StaticCounter.getAndIncrease(), minLength)

if(taint.length !in minLength..maxLength){
throw IllegalStateException("Tainted value out of min-max range [$minLength,$maxLength]")
}
value = taint
tainted = true
}

Expand Down
2 changes: 1 addition & 1 deletion docs/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ There are 3 types of options:
|`enableRPCAssertionWithInstance`| __Boolean__. Whether to generate RPC Assertions based on response instance. *Default value*: `true`.|
|`enableRPCCustomizedResponseTargets`| __Boolean__. Whether to enable customized responses indicating business logic. *Default value*: `true`.|
|`enableRPCExtraResponseTargets`| __Boolean__. Whether to enable extra targets for responses, e.g., regarding nullable response, having extra targets for whether it is null. *Default value*: `true`.|
|`enableSchemaConstraintHandling`| __Boolean__. Whether to employ constraints specified in API schema (e.g., OpenAPI) in test generation. *Default value*: `true`.|
|`enableTrackEvaluatedIndividual`| __Boolean__. Whether to enable tracking the history of modifications of the individuals with its fitness values (i.e., evaluated individual) during the search. Note that we enforced that set enableTrackIndividual false when enableTrackEvaluatedIndividual is true since information of individual is part of evaluated individual. *Default value*: `true`.|
|`enableTrackIndividual`| __Boolean__. Whether to enable tracking the history of modifications of the individuals during the search. *Default value*: `false`.|
|`enableWeightBasedMutationRateSelectionForGene`| __Boolean__. Specify whether to enable weight-based mutation selection for selecting genes to mutate for a gene. *Default value*: `true`.|
Expand Down Expand Up @@ -205,7 +206,6 @@ There are 3 types of options:
|`enableAdaptiveResourceStructureMutation`| __Boolean__. Specify whether to decide the resource-based structure mutator and resource to be mutated adaptively based on impacts during focused search.Note that it only works when resource-based solution is enabled for solving REST problem. *Default value*: `false`.|
|`enableCustomizedMethodForMockObjectHandling`| __Boolean__. Whether to apply customized method (i.e., implement 'customizeMockingRPCExternalService' for external services or 'customizeMockingDatabase' for database) to handle mock object. *Default value*: `false`.|
|`enableRPCCustomizedTestOutput`| __Boolean__. Whether to enable customized RPC Test output if 'customizeRPCTestOutput' is implemented. *Default value*: `false`.|
|`enableSchemaConstraintHandling`| __Boolean__. Whether to employ constraints specified in API schema (e.g., OpenAPI) in test generation. *Default value*: `false`.|
|`enableWriteSnapshotTests`| __Boolean__. Enable to print snapshots of the generated tests during the search in an interval defined in snapshotsInterval. *Default value*: `false`.|
|`externalRequestHarvesterNumberOfThreads`| __Int__. Number of threads for external request harvester. No more threads than numbers of processors will be used. *Constraints*: `min=1.0`. *Default value*: `2`.|
|`externalRequestResponseSelectionStrategy`| __Enum__. Harvested external request response selection strategy. *Valid values*: `EXACT, CLOSEST_SAME_DOMAIN, CLOSEST_SAME_PATH, RANDOM`. *Default value*: `EXACT`.|
Expand Down
4 changes: 4 additions & 0 deletions e2e-tests/spring-rest-openapi-v2/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.foo.rest.examples.spring.stringminlenght;

import com.foo.rest.examples.spring.SwaggerConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@EnableSwagger2
@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
public class StringMinLengthApplication extends SwaggerConfiguration {

public static void main(String[] args) {
SpringApplication.run(StringMinLengthApplication.class, args);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.foo.rest.examples.spring.stringminlenght;

import com.foo.rest.examples.spring.strings.StringsResponseDto;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;
import javax.validation.constraints.Min;
import javax.validation.constraints.Size;
import javax.ws.rs.core.MediaType;

@Validated
@RestController
@RequestMapping(path = "/api/minlength")
public class StringMinLengthRest {


@RequestMapping(
value = "/{s}",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON
)
public String min20(
@PathVariable("s") @Valid @Size(min = 20) String s
){

return "OK";
}



}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"swagger": "2.0",
"info": {
"description": "Some description",
"version": "1.0",
"title": "API"
},
"host": "localhost:8080",
"basePath": "/",
"tags": [
{
"name": "string-min-length-rest",
"description": "String Min Length Rest"
}
],
"paths": {
"/api/minlength/{s}": {
"get": {
"tags": [
"string-min-length-rest"
],
"summary": "min20",
"operationId": "min20UsingGET",
"produces": [
"application/json"
],
"parameters": [
{
"name": "s",
"in": "path",
"description": "s",
"required": true,
"type": "string",
"minLength" : 20
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
},
"404": {
"description": "Not Found"
}
},
"responsesObject": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
},
"404": {
"description": "Not Found"
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.foo.rest.examples.spring.stringminlength;

import com.foo.rest.examples.spring.SpringController;
import com.foo.rest.examples.spring.stringminlenght.StringMinLengthApplication;
import org.evomaster.client.java.controller.problem.ProblemInfo;
import org.evomaster.client.java.controller.problem.RestProblem;

public class StringMinLengthController extends SpringController {

public StringMinLengthController(){
super(StringMinLengthApplication.class);
}

@Override
public ProblemInfo getProblemInfo() {
return new RestProblem(
"http://localhost:" + getSutPort() + "/swagger-minlength.json",
null
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.evomaster.e2etests.spring.examples.stringminlength;

import com.foo.rest.examples.spring.stringminlength.StringMinLengthController;
import com.foo.rest.examples.spring.strings.StringsController;
import org.evomaster.core.problem.rest.HttpVerb;
import org.evomaster.core.problem.rest.RestIndividual;
import org.evomaster.core.search.Solution;
import org.evomaster.e2etests.spring.examples.SpringTestBase;
import org.evomaster.e2etests.spring.examples.strings.StringsTestBase;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertTrue;

public class StringMinLengthEMTest extends SpringTestBase {

@BeforeAll
public static void initClass() throws Exception {
SpringTestBase.initClass(new StringMinLengthController());
}

@Test
public void testRunEM() throws Throwable {

runTestHandlingFlakyAndCompilation(
"StringMinLengthEM",
"org.bar.StringMinLengthEM",
10,
(args) -> {
Solution<RestIndividual> solution = initAndRun(args);

assertTrue(solution.getIndividuals().size() >= 1);

assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/api/minlength/{s}", "OK");
});
}
}

0 comments on commit ef1d018

Please sign in to comment.