diff --git a/build-parent/pom.xml b/build-parent/pom.xml index 73bfdf7f3..b1b2e8f64 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -62,6 +62,7 @@ limitations under the License. de/learnlib/filter/reuse/**/Reuse*Builder.class + de/learnlib/oracle/equivalence/KWay*Builder.class de/learnlib/filter/cache/**/*Interning*.class diff --git a/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/KWayStateCoverEQOracle.java b/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/KWayStateCoverEQOracle.java new file mode 100644 index 000000000..ee2c31dcf --- /dev/null +++ b/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/KWayStateCoverEQOracle.java @@ -0,0 +1,141 @@ +/* Copyright (C) 2013-2025 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.oracle.equivalence; + +import java.util.Collection; +import java.util.Random; +import java.util.stream.Stream; + +import de.learnlib.oracle.EquivalenceOracle; +import de.learnlib.oracle.MembershipOracle; +import de.learnlib.tooling.annotation.builder.GenerateBuilder; +import net.automatalib.automaton.DeterministicAutomaton; +import net.automatalib.automaton.concept.Output; +import net.automatalib.common.util.collection.IteratorUtil; +import net.automatalib.util.automaton.conformance.KWayStateCoverTestsIterator; +import net.automatalib.util.automaton.conformance.KWayStateCoverTestsIterator.CombinationMethod; +import net.automatalib.word.Word; + +/** + * An {@link EquivalenceOracle} based on the concepts of mutation testing as described in the paper Learning from Faults: Mutation Testing in Active Automata + * Learning by Bernhard K. Aichernig and Martin Tappler. + *

+ * A test case will be computed for every k-combination or k-permutation of states with additional random walk at the + * end. + *

+ * Implementation detail: Note that this test generator heavily relies on the sampling of states. If the given + * automaton has very few or very many states, the number of generated test cases may be very low or high, respectively. + * As a result, it may be advisable to {@link EQOracleChain combine} this generator with other generators or limit the + * number of generated test cases. + * + * @param + * automaton type + * @param + * input symbol type + * @param + * output domain + * + * @see KWayStateCoverTestsIterator + */ +public class KWayStateCoverEQOracle & Output, I, D> + extends AbstractTestWordEQOracle { + + private final Random random; + private final int randomWalkLen; + private final int k; + private final CombinationMethod combinationMethod; + + /** + * Constructor. + * + * @param oracle + * the oracle for accessing the system under learning + * @param random + * the random number generator to use + * @param randomWalkLen + * length of random walk performed at the end of each combination/permutation + * @param k + * k value used for k-wise combinations/permutations of states + * @param combinationMethod + * the method for computing combinations + * @param batchSize + * size of the batches sent to the membership oracle + * + * @see KWayStateCoverTestsIterator#KWayStateCoverTestsIterator(DeterministicAutomaton, Collection, Random, int, + * int, CombinationMethod) + */ + @GenerateBuilder(defaults = BuilderDefaults.class) + public KWayStateCoverEQOracle(MembershipOracle oracle, + Random random, + int randomWalkLen, + int k, + CombinationMethod combinationMethod, + int batchSize) { + super(oracle, batchSize); + this.random = random; + this.randomWalkLen = randomWalkLen; + this.k = k; + this.combinationMethod = combinationMethod; + } + + @Override + public Stream> generateTestWords(A hypothesis, Collection inputs) { + final DeterministicAutomaton casted = hypothesis; + return doGenerateTestWords(casted, inputs, this.random, this.randomWalkLen, this.k, this.combinationMethod); + } + + private static , S, I> Stream> doGenerateTestWords(A hypothesis, + Collection inputs, + Random random, + int randomWalkLen, + int k, + CombinationMethod combinationMethod) { + return IteratorUtil.stream(new KWayStateCoverTestsIterator<>(hypothesis, + inputs, + random, + randomWalkLen, + k, + combinationMethod)); + } + + static final class BuilderDefaults { + + private BuilderDefaults() { + // prevent instantiation + } + + static Random random() { + return new Random(); + } + + static int batchSize() { + return 1; + } + + static int k() { + return KWayStateCoverTestsIterator.DEFAULT_K; + } + + static int randomWalkLen() { + return KWayStateCoverTestsIterator.DEFAULT_R_WALK_LEN; + } + + static CombinationMethod combinationMethod() { + return CombinationMethod.PERMUTATIONS; + } + } +} diff --git a/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/KWayTransitionCoverEQOracle.java b/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/KWayTransitionCoverEQOracle.java new file mode 100644 index 000000000..d8cf93e1d --- /dev/null +++ b/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/KWayTransitionCoverEQOracle.java @@ -0,0 +1,198 @@ +/* Copyright (C) 2013-2025 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.oracle.equivalence; + +import java.util.Collection; +import java.util.Random; +import java.util.stream.Stream; + +import de.learnlib.oracle.EquivalenceOracle; +import de.learnlib.oracle.MembershipOracle; +import de.learnlib.tooling.annotation.builder.GenerateBuilder; +import net.automatalib.automaton.DeterministicAutomaton; +import net.automatalib.automaton.concept.Output; +import net.automatalib.common.util.collection.IteratorUtil; +import net.automatalib.util.automaton.conformance.KWayTransitionCoverTestsIterator; +import net.automatalib.util.automaton.conformance.KWayTransitionCoverTestsIterator.GenerationMethod; +import net.automatalib.util.automaton.conformance.KWayTransitionCoverTestsIterator.OptimizationMetric; +import net.automatalib.word.Word; + +/** + * An {@link EquivalenceOracle} based on the concepts of mutation testing as described in the paper Learning from Faults: Mutation Testing in Active Automata + * Learning by Bernhard K. Aichernig and Martin Tappler. + *

+ * This Equivalence oracle selects test cases based on k-way transitions coverage. It does that by generating random + * queries and finding the smallest subset with the highest coverage. In other words, this oracle finds counter examples + * by running random paths that cover all pairwise / k-way transitions. + *

+ * Implementation detail: Note that this test generator heavily relies on the sampling of states. If the given + * automaton has very few or very many states, the number of generated test cases may be very low or high, respectively. + * As a result, it may be advisable to {@link EQOracleChain combine} this generator with other generators or limit the + * number of generated test cases. + * + * @param + * automaton type + * @param + * input symbol type + * @param + * output domain + * + * @see KWayTransitionCoverTestsIterator + */ +public class KWayTransitionCoverEQOracle & Output, I, D> + extends AbstractTestWordEQOracle { + + private final Random random; + private final int randomWalkLen; + private final int numGeneratePaths; + private final int maxPathLen; + private final int maxNumberOfSteps; + private final int k; + private final OptimizationMetric optimizationMetric; + private final GenerationMethod generationMethod; + + /** + * Constructor. + * + * @param oracle + * the oracle for accessing the system under learning + * @param random + * the random number generator to use + * @param randomWalkLen + * the number of steps that are added by {@link GenerationMethod#PREFIX prefix}-generated paths + * @param numGeneratePaths + * number of {@link GenerationMethod#RANDOM randomly}-generated queries used to find the optimal subset + * @param maxPathLen + * the maximum step size of {@link GenerationMethod#RANDOM randomly}-generated queries + * @param maxNumberOfSteps + * threshold for the number of steps after which no more new test words will be generated (a value less than + * zero means no limit) + * @param k + * k value used for K-Way transitions, i.e the number of steps between the start and the end of a + * transition + * @param optimizationMetric + * the metric after which test queries are minimized + * @param generationMethod + * defines how the queries are generated + * @param batchSize + * size of the batches sent to the membership oracle + * + * @see KWayTransitionCoverTestsIterator#KWayTransitionCoverTestsIterator(DeterministicAutomaton, Collection, + * Random, int, int, int, int, int, OptimizationMetric, GenerationMethod) + */ + @GenerateBuilder(defaults = BuilderDefaults.class) + public KWayTransitionCoverEQOracle(MembershipOracle oracle, + Random random, + int randomWalkLen, + int numGeneratePaths, + int maxPathLen, + int maxNumberOfSteps, + int k, + OptimizationMetric optimizationMetric, + GenerationMethod generationMethod, + int batchSize) { + super(oracle, batchSize); + this.random = random; + this.randomWalkLen = randomWalkLen; + this.numGeneratePaths = numGeneratePaths; + this.maxPathLen = maxPathLen; + this.maxNumberOfSteps = maxNumberOfSteps; + this.k = k; + this.optimizationMetric = optimizationMetric; + this.generationMethod = generationMethod; + } + + @Override + public Stream> generateTestWords(A hypothesis, Collection inputs) { + final DeterministicAutomaton casted = hypothesis; + return doGenerateTestWords(casted, + inputs, + this.random, + this.randomWalkLen, + this.numGeneratePaths, + this.maxPathLen, + this.maxNumberOfSteps, + this.k, + this.optimizationMetric, + this.generationMethod); + } + + private static , S, I> Stream> doGenerateTestWords(A hypothesis, + Collection inputs, + Random random, + int randomWalkLen, + int numGeneratePaths, + int maxPathLen, + int maxNumberOfSteps, + int k, + OptimizationMetric optimizationMetric, + GenerationMethod generationMethod) { + return IteratorUtil.stream(new KWayTransitionCoverTestsIterator<>(hypothesis, + inputs, + random, + randomWalkLen, + numGeneratePaths, + maxPathLen, + maxNumberOfSteps, + k, + optimizationMetric, + generationMethod)); + } + + static final class BuilderDefaults { + + private BuilderDefaults() { + // prevent instantiation + } + + static Random random() { + return new Random(); + } + + static int batchSize() { + return 1; + } + + static int k() { + return KWayTransitionCoverTestsIterator.DEFAULT_K; + } + + static GenerationMethod generationMethod() { + return GenerationMethod.RANDOM; + } + + static int numGeneratePaths() { + return KWayTransitionCoverTestsIterator.DEFAULT_NUM_GEN_PATHS; + } + + static int maxPathLen() { + return KWayTransitionCoverTestsIterator.DEFAULT_MAX_PATH_LENGTH; + } + + static int maxNumberOfSteps() { + return KWayTransitionCoverTestsIterator.DEFAULT_MAX_NUM_STEPS; + } + + static OptimizationMetric optimizationMetric() { + return OptimizationMetric.STEPS; + } + + static int randomWalkLen() { + return KWayTransitionCoverTestsIterator.DEFAULT_R_WALK_LEN; + } + } +} diff --git a/oracles/equivalence-oracles/src/test/java/de/learnlib/oracle/equivalence/KWayStateCoverEQOracleTest.java b/oracles/equivalence-oracles/src/test/java/de/learnlib/oracle/equivalence/KWayStateCoverEQOracleTest.java new file mode 100644 index 000000000..0977a2e0f --- /dev/null +++ b/oracles/equivalence-oracles/src/test/java/de/learnlib/oracle/equivalence/KWayStateCoverEQOracleTest.java @@ -0,0 +1,54 @@ +/* Copyright (C) 2013-2025 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.oracle.equivalence; + +import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; + +import de.learnlib.TestWordGenerator; +import net.automatalib.alphabet.Alphabet; +import net.automatalib.alphabet.impl.Alphabets; +import net.automatalib.automaton.fsa.DFA; +import net.automatalib.automaton.fsa.impl.CompactDFA; +import net.automatalib.common.util.collection.IteratorUtil; +import net.automatalib.util.automaton.conformance.KWayStateCoverTestsIterator; +import net.automatalib.util.automaton.random.RandomAutomata; +import net.automatalib.word.Word; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class KWayStateCoverEQOracleTest { + + private static final int SIZE = 10; + + @Test + public void testOracle() { + final int seed = 42; + final Alphabet alphabet = Alphabets.characters('a', 'c'); + final CompactDFA dfa = RandomAutomata.randomDFA(new Random(seed), SIZE, alphabet); + + final TestWordGenerator, Character> oracle = + new KWayStateCoverEQOracleBuilder, Character, Boolean>().withRandom(new Random( + seed)).create(); + + List> tests = oracle.generateTestWords(dfa, alphabet).collect(Collectors.toList()); + List> iter = + IteratorUtil.list(new KWayStateCoverTestsIterator<>(dfa, alphabet, new Random(seed))); + + Assert.assertEquals(tests, iter); + } +} diff --git a/oracles/equivalence-oracles/src/test/java/de/learnlib/oracle/equivalence/KWayTransitionCoverEQOracleTest.java b/oracles/equivalence-oracles/src/test/java/de/learnlib/oracle/equivalence/KWayTransitionCoverEQOracleTest.java new file mode 100644 index 000000000..f527f446a --- /dev/null +++ b/oracles/equivalence-oracles/src/test/java/de/learnlib/oracle/equivalence/KWayTransitionCoverEQOracleTest.java @@ -0,0 +1,54 @@ +/* Copyright (C) 2013-2025 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.oracle.equivalence; + +import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; + +import de.learnlib.TestWordGenerator; +import net.automatalib.alphabet.Alphabet; +import net.automatalib.alphabet.impl.Alphabets; +import net.automatalib.automaton.fsa.DFA; +import net.automatalib.automaton.fsa.impl.CompactDFA; +import net.automatalib.common.util.collection.IteratorUtil; +import net.automatalib.util.automaton.conformance.KWayTransitionCoverTestsIterator; +import net.automatalib.util.automaton.random.RandomAutomata; +import net.automatalib.word.Word; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class KWayTransitionCoverEQOracleTest { + + private static final int SIZE = 10; + + @Test + public void testOracle() { + final int seed = 42; + final Alphabet alphabet = Alphabets.characters('a', 'c'); + final CompactDFA dfa = RandomAutomata.randomDFA(new Random(seed), SIZE, alphabet); + + final TestWordGenerator, Character> oracle = + new KWayTransitionCoverEQOracleBuilder, Character, Boolean>().withRandom(new Random( + seed)).create(); + + List> tests = oracle.generateTestWords(dfa, alphabet).collect(Collectors.toList()); + List> iter = + IteratorUtil.list(new KWayTransitionCoverTestsIterator<>(dfa, alphabet, new Random(seed))); + + Assert.assertEquals(tests, iter); + } +}