diff --git a/.gitignore b/.gitignore index f7671a5f39..42269a9177 100644 --- a/.gitignore +++ b/.gitignore @@ -169,3 +169,4 @@ Migrations/ /e2e-tests/emb-json/target/ /process_data/ /e2e-tests/spring-rest-multidb/target/ +.DS_Store diff --git a/core/src/main/kotlin/org/evomaster/core/EMConfig.kt b/core/src/main/kotlin/org/evomaster/core/EMConfig.kt index 0fead3b4bf..8b113a7f5c 100644 --- a/core/src/main/kotlin/org/evomaster/core/EMConfig.kt +++ b/core/src/main/kotlin/org/evomaster/core/EMConfig.kt @@ -1062,7 +1062,7 @@ class EMConfig { var avoidNonDeterministicLogs = false enum class Algorithm { - DEFAULT, SMARTS, MIO, RANDOM, WTS, MOSA, RW + DEFAULT, SMARTS, MIO, RANDOM, WTS, MOSA, RW, STANDARD_GA } @Cfg("The algorithm used to generate test cases. The default depends on whether black-box or white-box testing is done.") @@ -1308,6 +1308,10 @@ class EMConfig { @Min(1.0) var populationSize = 30 + @Cfg("Fixed mutation rate") + @Probability + var fixedRateMutation = 0.04 + @Cfg("Define the maximum number of tests in a suite in the search algorithms that evolve whole suites, e.g. WTS") @Min(1.0) var maxSearchSuiteSize = 50 @@ -2406,6 +2410,9 @@ class EMConfig { @Cfg("Max length for test comments. Needed when enumerating some names/values, making comments too long to be" + " on a single line") var maxLengthForCommentLine = 80 + @Cfg(description = "Number of elite individuals to be preserved when forming the next population in population-based search algorithms that do not use an archive, like for example Genetic Algorithms") + @Min(0.0) + var elitesCount: Int = 1 fun getProbabilityUseDataPool() : Double{ return if(blackBox){ diff --git a/core/src/main/kotlin/org/evomaster/core/Main.kt b/core/src/main/kotlin/org/evomaster/core/Main.kt index 03bb7137e7..bde5819e6a 100644 --- a/core/src/main/kotlin/org/evomaster/core/Main.kt +++ b/core/src/main/kotlin/org/evomaster/core/Main.kt @@ -12,7 +12,6 @@ import org.evomaster.core.AnsiColor.Companion.inRed import org.evomaster.core.AnsiColor.Companion.inYellow import org.evomaster.core.config.ConfigProblemException import org.evomaster.core.logging.LoggingUtil -import org.evomaster.core.output.OutputFormat import org.evomaster.core.output.Termination import org.evomaster.core.output.TestSuiteSplitter import org.evomaster.core.output.clustering.SplitResult @@ -35,6 +34,7 @@ import org.evomaster.core.remote.service.RemoteController import org.evomaster.core.remote.service.RemoteControllerImplementation import org.evomaster.core.search.Solution import org.evomaster.core.search.algorithms.* +import org.evomaster.core.search.algorithms.* import org.evomaster.core.search.service.* import org.evomaster.core.search.service.monitor.SearchProcessMonitor import org.evomaster.core.search.service.mutator.genemutation.ArchiveImpactSelector @@ -492,6 +492,10 @@ class Main { Key.get(object : TypeLiteral>() {}) EMConfig.Algorithm.RW -> Key.get(object : TypeLiteral>() {}) + EMConfig.Algorithm.STANDARD_GA -> + Key.get(object : TypeLiteral>() {}) + + else -> throw IllegalStateException("Unrecognized algorithm ${config.algorithm}") } } @@ -511,6 +515,10 @@ class Main { Key.get(object : TypeLiteral>() {}) EMConfig.Algorithm.RW -> Key.get(object : TypeLiteral>() {}) + EMConfig.Algorithm.STANDARD_GA -> + Key.get(object : TypeLiteral>() {}) + + else -> throw IllegalStateException("Unrecognized algorithm ${config.algorithm}") } } @@ -530,6 +538,9 @@ class Main { Key.get(object : TypeLiteral>() {}) EMConfig.Algorithm.RW -> Key.get(object : TypeLiteral>() {}) + EMConfig.Algorithm.STANDARD_GA -> + Key.get(object : TypeLiteral>() {}) + else -> throw IllegalStateException("Unrecognized algorithm ${config.algorithm}") } } @@ -549,6 +560,9 @@ class Main { Key.get(object : TypeLiteral>() {}) EMConfig.Algorithm.RW -> Key.get(object : TypeLiteral>() {}) + EMConfig.Algorithm.STANDARD_GA -> + Key.get(object : TypeLiteral>() {}) + else -> throw IllegalStateException("Unrecognized algorithm ${config.algorithm}") } } diff --git a/core/src/main/kotlin/org/evomaster/core/search/algorithms/AbstractGeneticAlgorithm.kt b/core/src/main/kotlin/org/evomaster/core/search/algorithms/AbstractGeneticAlgorithm.kt new file mode 100644 index 0000000000..b775ba1e98 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/search/algorithms/AbstractGeneticAlgorithm.kt @@ -0,0 +1,109 @@ +package org.evomaster.core.search.algorithms + +import org.evomaster.core.search.Individual +import org.evomaster.core.search.algorithms.wts.WtsEvalIndividual +import org.evomaster.core.search.service.SearchAlgorithm + +abstract class AbstractGeneticAlgorithm: SearchAlgorithm() where T : Individual { + + private val population: MutableList> = mutableListOf() + + override fun setupBeforeSearch() { + population.clear() + + initPopulation() + } + + protected open fun initPopulation() { + + val n = config.populationSize + + for (i in 1..n) { + population.add(sampleSuite()) + + if (!time.shouldContinueSearch()) { + break + } + } + } + + protected fun formTheNextPopulation(population: MutableList>): MutableList> { + + val nextPop: MutableList> = mutableListOf() + + if (config.elitesCount > 0 && population.isNotEmpty()) { + var sortedPopulation = population.sortedByDescending { it.calculateCombinedFitness() } + + var elites = sortedPopulation.take(config.elitesCount) + + nextPop.addAll(elites) + } + + return nextPop + } + + protected fun mutate(wts: WtsEvalIndividual) { + + val op = randomness.choose(listOf("del", "add", "mod")) + val n = wts.suite.size + when (op) { + "del" -> if (n > 1) { + val i = randomness.nextInt(n) + wts.suite.removeAt(i) + } + "add" -> if (n < config.maxSearchSuiteSize) { + ff.calculateCoverage(sampler.sample(), modifiedSpec = null)?.run { + archive.addIfNeeded(this) + wts.suite.add(this) + } + } + "mod" -> { + val i = randomness.nextInt(n) + val ind = wts.suite[i] + + getMutatator().mutateAndSave(ind, archive) + ?.let { wts.suite[i] = it } + } + } + } + + protected fun xover(x: WtsEvalIndividual, y: WtsEvalIndividual) { + + val nx = x.suite.size + val ny = y.suite.size + + val splitPoint = randomness.nextInt(Math.min(nx, ny)) + + (0..splitPoint).forEach { + val k = x.suite[it] + x.suite[it] = y.suite[it] + y.suite[it] = k + } + } + + protected fun tournamentSelection(): WtsEvalIndividual{ + val selectedIndividuals = randomness.choose(population, config.tournamentSize) + val bestIndividual = selectedIndividuals.maxByOrNull { it.calculateCombinedFitness() } + return bestIndividual ?: randomness.choose(population) + } + + private fun sampleSuite(): WtsEvalIndividual { + + val n = 1 + randomness.nextInt(config.maxSearchSuiteSize) + + val suite = WtsEvalIndividual(mutableListOf()) + + for (i in 1..n) { + ff.calculateCoverage(sampler.sample(), modifiedSpec = null)?.run { + archive.addIfNeeded(this) + suite.suite.add(this) + } + + if (!time.shouldContinueSearch()) { + break + } + } + + return suite + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/search/algorithms/StandardGeneticAlgorithm.kt b/core/src/main/kotlin/org/evomaster/core/search/algorithms/StandardGeneticAlgorithm.kt new file mode 100644 index 0000000000..396bee2e27 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/search/algorithms/StandardGeneticAlgorithm.kt @@ -0,0 +1,57 @@ +package org.evomaster.core.search.algorithms + +import org.evomaster.core.EMConfig +import org.evomaster.core.search.Individual +import org.evomaster.core.search.algorithms.wts.WtsEvalIndividual + +/** + * An implementation of the Standard Genetic algorithm, + */ +open class StandardGeneticAlgorithm : AbstractGeneticAlgorithm() where T : Individual { + + + private val population: MutableList> = mutableListOf() + + override fun getType(): EMConfig.Algorithm { + return EMConfig.Algorithm.STANDARD_GA + } + + override fun searchOnce() { + + val n = config.populationSize + + val nextPop = formTheNextPopulation(population) + + while (nextPop.size < n) { + + val sizeBefore = nextPop.size + + val x = tournamentSelection() + val y = tournamentSelection() + + if (randomness.nextBoolean(config.xoverProbability)) { + xover(x, y) + } + + if(randomness.nextBoolean(config.fixedRateMutation)){ + mutate(x) + } + + if(randomness.nextBoolean(config.fixedRateMutation)){ + mutate(y) + } + + nextPop.add(x) + nextPop.add(y) + + assert(nextPop.size == sizeBefore + 2) + + if (!time.shouldContinueSearch()) { + break + } + } + + population.clear() + population.addAll(nextPop) + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/search/algorithms/WtsAlgorithm.kt b/core/src/main/kotlin/org/evomaster/core/search/algorithms/WtsAlgorithm.kt index f9387d6b20..959d6791cd 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/algorithms/WtsAlgorithm.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/algorithms/WtsAlgorithm.kt @@ -19,7 +19,7 @@ import org.evomaster.core.search.service.SearchAlgorithm * of search algorithms, and not really something to * use regularly in EvoMaster */ -class WtsAlgorithm : SearchAlgorithm() where T : Individual { +open class WtsAlgorithm : SearchAlgorithm() where T : Individual { private val population: MutableList> = mutableListOf() diff --git a/core/src/test/kotlin/org/evomaster/core/search/algorithms/StandardGeneticAlgorithmTest.kt b/core/src/test/kotlin/org/evomaster/core/search/algorithms/StandardGeneticAlgorithmTest.kt new file mode 100644 index 0000000000..b883688d7f --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/search/algorithms/StandardGeneticAlgorithmTest.kt @@ -0,0 +1,49 @@ +package org.evomaster.core.search.algorithms + +import com.google.inject.Injector +import com.google.inject.Key +import com.google.inject.Module +import com.google.inject.TypeLiteral +import com.netflix.governator.guice.LifecycleInjector +import org.evomaster.core.BaseModule +import org.evomaster.core.EMConfig +import org.evomaster.core.TestUtils +import org.evomaster.core.search.algorithms.onemax.OneMaxIndividual +import org.evomaster.core.search.algorithms.onemax.OneMaxModule +import org.evomaster.core.search.algorithms.onemax.OneMaxSampler +import org.evomaster.core.search.service.ExecutionPhaseController +import org.junit.jupiter.api.Test + +import org.junit.jupiter.api.Assertions.* + +class StandardGeneticAlgorithmTest { + + val injector: Injector = LifecycleInjector.builder() + .withModules(* arrayOf(OneMaxModule(), BaseModule())) + .build().createInjector() + + @Test + fun testStandardGeneticAlgorithm() { + TestUtils.handleFlaky { + val standardGeneticAlgorithm = injector.getInstance( + Key.get( + object : TypeLiteral>() {}) + ) + + val config = injector.getInstance(EMConfig::class.java) + config.maxEvaluations = 10000 + config.stoppingCriterion = EMConfig.StoppingCriterion.ACTION_EVALUATIONS + + val epc = injector.getInstance(ExecutionPhaseController::class.java) + epc.startSearch() + + val solution = standardGeneticAlgorithm.search() + + epc.finishSearch() + + assertTrue(solution.individuals.size == 1) + assertEquals(OneMaxSampler.DEFAULT_N.toDouble(), solution.overall.computeFitnessScore(), 0.001) + } + } + +} \ No newline at end of file diff --git a/core/src/test/kotlin/org/evomaster/core/search/algorithms/onemax/OneMaxSampler.kt b/core/src/test/kotlin/org/evomaster/core/search/algorithms/onemax/OneMaxSampler.kt index 0e8c525b95..418c55f11b 100644 --- a/core/src/test/kotlin/org/evomaster/core/search/algorithms/onemax/OneMaxSampler.kt +++ b/core/src/test/kotlin/org/evomaster/core/search/algorithms/onemax/OneMaxSampler.kt @@ -6,7 +6,12 @@ import org.evomaster.core.search.service.Sampler class OneMaxSampler : Sampler(){ - var n = 3 + companion object{ + const val DEFAULT_N = 3 + } + + + var n = DEFAULT_N override fun sampleAtRandom(): OneMaxIndividual { diff --git a/e2e-tests/spring-rest-openapi-v2/src/test/java/org/evomaster/e2etests/spring/examples/positiveinteger/PIEMTest.java b/e2e-tests/spring-rest-openapi-v2/src/test/java/org/evomaster/e2etests/spring/examples/positiveinteger/PIEMTest.java index bf21d4b02c..3cdb67f488 100644 --- a/e2e-tests/spring-rest-openapi-v2/src/test/java/org/evomaster/e2etests/spring/examples/positiveinteger/PIEMTest.java +++ b/e2e-tests/spring-rest-openapi-v2/src/test/java/org/evomaster/e2etests/spring/examples/positiveinteger/PIEMTest.java @@ -6,6 +6,9 @@ import org.evomaster.core.problem.rest.RestIndividual; import org.evomaster.core.search.Solution; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; import java.util.List; @@ -14,32 +17,18 @@ public class PIEMTest extends PITestBase { - - @Test - public void testMIO() throws Throwable { - testRunEM(EMConfig.Algorithm.MIO, 1000); - } - - @Test - public void testRand() throws Throwable { - testRunEM(EMConfig.Algorithm.RANDOM, 20); + @ParameterizedTest + @EnumSource(EMConfig.Algorithm.class) + public void testAlgorithms(EMConfig.Algorithm alg) throws Throwable { + testRunEM(alg, 1000);// high value, just to check if no crash } - @Test - public void testWTS() throws Throwable { - testRunEM(EMConfig.Algorithm.WTS, 2_000); // high value, just to check if no crash - } - - @Test - public void testMOSA() throws Throwable { - testRunEM(EMConfig.Algorithm.MOSA, 2_000); // high value, just to check if no crash - } private void testRunEM(EMConfig.Algorithm alg, int iterations) throws Throwable { String outputFolderName = "PIEM_" + alg.toString(); - ClassName className = new ClassName("org.PIEM_Run_" + alg.toString()); + ClassName className = new ClassName("org.PIEM_Run_" + alg); clearGeneratedFiles(outputFolderName, className); handleFlaky(() -> {