From bf4a347c104116bc9a6f1a45704b04bd671af8c6 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Mon, 28 Jul 2025 22:31:15 +0200 Subject: [PATCH 1/9] initial work on kWay tests --- .../KWayStateCoverTestsIterator.java | 211 +++++++++++ .../KWayTransitionCoverTestsIterator.java | 348 ++++++++++++++++++ .../util/automaton/procedural/SPAs.java | 2 +- .../net/automatalib/util/graph/Graphs.java | 19 + 4 files changed, 579 insertions(+), 1 deletion(-) create mode 100644 util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java create mode 100644 util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java diff --git a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java new file mode 100644 index 000000000..ac5783692 --- /dev/null +++ b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java @@ -0,0 +1,211 @@ +package net.automatalib.util.automaton.conformance; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Random; +import java.util.Set; +import net.automatalib.automaton.UniversalDeterministicAutomaton; +import net.automatalib.automaton.graph.TransitionEdge; +import net.automatalib.common.util.HashUtil; +import net.automatalib.common.util.collection.AbstractSimplifiedIterator; +import net.automatalib.common.util.collection.CollectionUtil; +import net.automatalib.common.util.collection.IterableUtil; +import net.automatalib.common.util.collection.IteratorUtil; +import net.automatalib.util.graph.Graphs; +import net.automatalib.util.graph.apsp.APSPResult; +import net.automatalib.word.Word; +import net.automatalib.word.WordBuilder; + +public class KWayStateCoverTestsIterator> + implements Iterator> { + + private final A automaton; + private final List alphabet; + private final Random random; + private final int k; + private final int randomWalkLen; + private final CombinationMethod method; + + private final Iterator> iterator; + + public KWayStateCoverTestsIterator(A automaton, Collection inputs) { + this(automaton, inputs, new Random()); + } + + public KWayStateCoverTestsIterator(A automaton, Collection inputs, Random random) { + this(automaton, inputs, random, 2, 20, CombinationMethod.Permutations); + } + + public KWayStateCoverTestsIterator(A automaton, + Collection inputs, + Random random, + int k, + int randomWalkLen, + CombinationMethod method) { + this.automaton = automaton; + this.alphabet = CollectionUtil.randomAccessList(inputs); + this.k = k; + this.randomWalkLen = randomWalkLen; + this.method = method; + this.random = random; + + if (automaton.size() == 0) { + this.iterator = Collections.emptyIterator(); + } else { + final FirstPhaseIterator firstIterator = new FirstPhaseIterator(); + final SecondPhaseIterator secondPhaseIterator = new SecondPhaseIterator(); + this.iterator = IteratorUtil.concat(firstIterator, secondPhaseIterator); + } + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public Word next() { + return iterator.next(); + } + + static Word getRandomChoices(List alphabet, int count, Random random) { + WordBuilder choices = new WordBuilder<>(count); + + for (int i = 0; i < count; i++) { + choices.add(alphabet.get(random.nextInt(alphabet.size()))); + } + + return choices.toWord(); + } + + /** + * Performs random walks if the automaton only has a single state. + */ + private final class FirstPhaseIterator extends AbstractSimplifiedIterator> { + + private int idx = 0; + + @Override + protected boolean calculateNext() { + if (automaton.size() == 1) { + if (idx++ < randomWalkLen) { + super.nextValue = getRandomChoices(alphabet, randomWalkLen, random); + return true; + } + } + return false; + } + } + + /** + * Performs the actual k-way coverage for automata with more than a single state. + */ + private final class SecondPhaseIterator extends AbstractSimplifiedIterator> { + + private final Iterator> combIter; + private final Set>>> cache; + + private APSPResult> apsp; + + public SecondPhaseIterator() { + List states = new ArrayList<>(automaton.getStates()); + Collections.shuffle(states, random); + this.combIter = method.getCombinations(states, k); + this.cache = new HashSet<>(); + } + + @Override + protected boolean calculateNext() { + + final S initial = automaton.getInitialState(); + final APSPResult> apsp = getAPSP(); + + while (combIter.hasNext()) { + final List comb = combIter.next(); + final Set>> prefixes = new HashSet<>(HashUtil.capacity(comb.size())); + + for (S c : comb) { + prefixes.add(apsp.getShortestPath(initial, c)); + } + + if (!cache.add(prefixes)) { + continue; + } + + final List> firstPath = apsp.getShortestPath(initial, comb.get(0)); + assert firstPath != null; + + final WordBuilder pathBuilder = new WordBuilder<>(); + for (TransitionEdge e : firstPath) { + pathBuilder.add(e.getInput()); + } + + /* + * in case of non-strongly connected automata test case might not be possible as a path between 2 states + * might not exist + */ + boolean possibleTestCase = true; + for (int index = 0; index < comb.size() - 1; index++) { + final List> pathBetweenStates = + apsp.getShortestPath(comb.get(index), comb.get(index + 1)); + + if (pathBetweenStates == null || pathBetweenStates.isEmpty()) { + possibleTestCase = false; + break; + } + + for (TransitionEdge pathBetweenState : pathBetweenStates) { + pathBuilder.append(pathBetweenState.getInput()); + } + } + + if (!possibleTestCase) { + continue; + } + + // Add random walk at the end + for (I p : getRandomChoices(alphabet, randomWalkLen, random)) { + pathBuilder.append(p); + } + + super.nextValue = pathBuilder.toWord(); + return true; + } + + return false; + } + + /** + * Compute all-pair-shortest-paths lazily, in case this iterator is never queried. + * + * @return the all-pair-shortest-paths result + */ + private APSPResult> getAPSP() { + if (this.apsp == null) { + this.apsp = Graphs.findAPSP(automaton.transitionGraphView(alphabet)); + } + return this.apsp; + } + } + + public enum CombinationMethod { + Combinations { + @Override + Iterator> getCombinations(List states, int k) { + throw new NoSuchMethodError("TODO"); + } + }, + Permutations { + @Override + Iterator> getCombinations(List states, int k) { + return IterableUtil.allTuples(states, k).iterator(); + } + }; + + abstract Iterator> getCombinations(List states, int k); + } +} diff --git a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java new file mode 100644 index 000000000..d5b9ab520 --- /dev/null +++ b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java @@ -0,0 +1,348 @@ +package net.automatalib.util.automaton.conformance; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Random; +import java.util.Set; +import net.automatalib.automaton.UniversalDeterministicAutomaton; +import net.automatalib.automaton.graph.TransitionEdge; +import net.automatalib.common.util.HashUtil; +import net.automatalib.common.util.collection.AbstractSimplifiedIterator; +import net.automatalib.common.util.collection.AbstractTwoLevelIterator; +import net.automatalib.common.util.collection.CollectionUtil; +import net.automatalib.common.util.collection.IterableUtil; +import net.automatalib.util.graph.Graphs; +import net.automatalib.util.graph.apsp.APSPResult; +import net.automatalib.word.Word; +import net.automatalib.word.WordBuilder; + +public class KWayTransitionCoverTestsIterator> + implements Iterator> { + + private final A automaton; + private final List alphabet; + private final Random random; + private final int k; + private final int numGeneratePaths; + private final int maxPathLen; + private final int maxNumberOfSteps; + private final Optimize optimize; + private final int randomWalkLen; + + // Cached paths for reuse + private final List> cachedPaths = new ArrayList<>(); + + private final Iterator> iterator; + + public KWayTransitionCoverTestsIterator(A automaton, Collection inputs) { + this(automaton, inputs, new Random()); + } + + public KWayTransitionCoverTestsIterator(A automaton, Collection inputs, Random random) { + this(automaton, inputs, random, 2, Method.Random, 1000, 50, 0, Optimize.Steps, 10); + } + + public KWayTransitionCoverTestsIterator(A automaton, + Collection inputs, + Random random, + int k, + Method method, + int numGeneratePaths, + int maxPathLen, + int maxNumberOfSteps, + Optimize optimize, + int randomWalkLen) { + this.automaton = automaton; + this.alphabet = CollectionUtil.randomAccessList(inputs); + this.random = random; + + this.k = k; + this.numGeneratePaths = numGeneratePaths; + this.maxPathLen = maxPathLen; + this.maxNumberOfSteps = maxNumberOfSteps; + this.optimize = optimize; + this.randomWalkLen = randomWalkLen; + + switch (method) { + case Prefix: + this.iterator = generatePrefixSteps(automaton).iterator(); + break; + case Random: + this.iterator = new GreedySetCoverIterator(); + break; + default: + throw new IllegalArgumentException("Unknown method " + method); + } + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public Word next() { + return iterator.next(); + } + + private Set> generateRandomPaths(A hypothesis) { + Set> result = new LinkedHashSet<>(HashUtil.capacity(numGeneratePaths)); + + for (int i = 0; i < numGeneratePaths; i++) { + int randomLength = random.nextInt(maxPathLen - k + 1) + k; // Ensuring length is at least `k` + Word steps = KWayStateCoverTestsIterator.getRandomChoices(alphabet, randomLength, random); + Path path = createPath(hypothesis, steps); + result.add(path); + } + + return result; + } + + /** + * * Creates Path object from provided automaton steps within specified Hypothesis context.** * *@return Newly + * created Path instance corresponding to provided step sequence.** + **/ + private Path createPath(A hypothesis, Word steps) { + Set> transitions = new HashSet<>(); + List> transitionsLog = new ArrayList<>(); + + List prevStates = new ArrayList<>(steps.size()); + List endStates = new ArrayList<>(steps.size()); + + S iter = hypothesis.getInitialState(); + + for (I i : steps) { + prevStates.add(iter); + iter = hypothesis.getSuccessor(iter, i); + endStates.add(iter); + } + + for (int i = 0; i < steps.size() - k + 1; i++) { + S prevState = prevStates.get(i); + S endState = endStates.get(i + k - 1); + Word chunk = steps.subWord(i, i + k); + + KWayTransition transition = new KWayTransition<>(prevState, endState, chunk); + + transitionsLog.add(transition); + transitions.add(transition); + } + + return new Path<>(hypothesis.getInitialState(), + endStates.get(endStates.size() - 1), + steps, + transitions, + transitionsLog); + } + + public Iterable> generatePrefixSteps(A hypothesis) { + List states = new ArrayList<>(hypothesis.getStates()); + Collections.reverse(states); + return () -> new PrefixStepsIterator(states.iterator()); + } + + /** + * Selects an optimal path from available candidates based on optimization strategy defined. + * + * @return Selected Path object or null if no suitable path is found. + **/ + private Path selectOptimalPath(Set> covered, Collection> paths) { + Path max = Collections.max(paths, optimize.getPathComparator(covered)); + return max.kWayTransitions.size() != covered.size() ? max : null; + } + + private static List getRandomChoices(List alphabet, int count, Random random) { + List choices = new ArrayList<>(count); + + for (int i = 0; i < count; i++) { + choices.add(alphabet.get(random.nextInt(alphabet.size()))); + } + + return choices; + } + + public enum Method { + Random, + Prefix + } + + public enum Optimize { + Steps { + @Override + Comparator> getPathComparator(Set> covered) { + return Comparator.comparingDouble(p -> ((double) (p.kWayTransitions.size() - covered.size())) / + p.steps.size()); + } + }, + Queries { + @Override + Comparator> getPathComparator(Set> covered) { + return Comparator.comparingDouble(p -> (p.kWayTransitions.size() - covered.size())); + } + }; + + abstract Comparator> getPathComparator(Set> covered); + } + + private class GreedySetCoverIterator extends AbstractSimplifiedIterator> { + + private final Set> paths; + private final int sizeOfUniverse; + private final Set> covered; + + private int stepCount; + + public GreedySetCoverIterator() { + this.paths = generateRandomPaths(automaton); + this.paths.addAll(cachedPaths); + this.covered = new HashSet<>(); + this.stepCount = 0; + this.sizeOfUniverse = automaton.getStates().size() * (int) Math.pow(alphabet.size(), k); + } + + @Override + protected boolean calculateNext() { + if (sizeOfUniverse > covered.size()) { + Path path = selectOptimalPath(covered, paths); + + if (path != null) { + covered.addAll(path.kWayTransitions); + paths.remove(path); + stepCount += path.steps.size(); +// result.add(path); + super.nextValue = path.steps; + return true; + } + + if (paths.isEmpty()) { + for (Word generatePrefixStep : generatePrefixSteps(automaton)) { + paths.add(createPath(automaton, generatePrefixStep)); + } + } + + if (maxNumberOfSteps != 0 && stepCount > maxNumberOfSteps) { + return false; + } + } + return false; + } + } + + private class PrefixStepsIterator extends AbstractTwoLevelIterator, Word> { + + private final APSPResult> apsp; + private final S initial; + + public PrefixStepsIterator(Iterator listIterator) { + super(listIterator); + this.apsp = Graphs.findAPSP(automaton.transitionGraphView(alphabet)); + this.initial = automaton.getInitialState(); + } + + @Override + protected Iterator> l2Iterator(S state) { + Iterable> lists = IterableUtil.allTuples(alphabet, k); + return lists.iterator(); + } + + @Override + protected Word combine(S state, List steps) { + List> prefix = apsp.getShortestPath(initial, state); + if (prefix == null) { + return Word.epsilon(); + } + + final WordBuilder wb = new WordBuilder<>(); + for (TransitionEdge edge : prefix) { + wb.append(edge.getInput()); + } + + wb.addAll(steps); + wb.addAll(getRandomChoices(alphabet, randomWalkLen, random)); + + return wb.toWord(); + } + } + + private static final class KWayTransition { + + private final S startState; + private final S endState; + private final Word steps; + + private KWayTransition(S startState, S endState, Word steps) { + this.startState = startState; + this.endState = endState; + this.steps = steps; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + + KWayTransition that = (KWayTransition) o; + return Objects.equals(startState, that.startState) && Objects.equals(endState, that.endState) && + Objects.equals(steps, that.steps); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(startState); + result = 31 * result + Objects.hashCode(endState); + result = 31 * result + Objects.hashCode(steps); + return result; + } + } + + private static final class Path { + + private final S startState; + private final S endState; + private final Word steps; + private final Set> kWayTransitions; + private final List> transitionsLog; + + private Path(S startState, + S endState, + Word steps, + Set> kWayTransitions, + List> transitionsLog) { + this.startState = startState; + this.endState = endState; + this.steps = steps; + this.kWayTransitions = kWayTransitions; + this.transitionsLog = transitionsLog; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + + Path path = (Path) o; + return Objects.equals(startState, path.startState) && Objects.equals(endState, path.endState) && + Objects.equals(steps, path.steps) && Objects.equals(kWayTransitions, path.kWayTransitions) && + Objects.equals(transitionsLog, path.transitionsLog); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(startState); + result = 31 * result + Objects.hashCode(endState); + result = 31 * result + Objects.hashCode(steps); + result = 31 * result + Objects.hashCode(kWayTransitions); + result = 31 * result + Objects.hashCode(transitionsLog); + return result; + } + } +} diff --git a/util/src/main/java/net/automatalib/util/automaton/procedural/SPAs.java b/util/src/main/java/net/automatalib/util/automaton/procedural/SPAs.java index 402e2b481..38cddbdce 100644 --- a/util/src/main/java/net/automatalib/util/automaton/procedural/SPAs.java +++ b/util/src/main/java/net/automatalib/util/automaton/procedural/SPAs.java @@ -277,7 +277,7 @@ private static List> exploreAccessSequences(DFA dfa, final List> result = new ArrayList<>(inputs.size()); final UniversalGraph, ?, ?> tgv = dfa.transitionGraphView(inputs); - final APSPResult> apsp = Graphs.findAPSP(tgv, edge -> 0F); + final APSPResult> apsp = Graphs.findAPSP(tgv); final List acceptingStates = new ArrayList<>(dfa.size()); for (S s : dfa) { diff --git a/util/src/main/java/net/automatalib/util/graph/Graphs.java b/util/src/main/java/net/automatalib/util/graph/Graphs.java index 663b495a5..bc3dd8eb5 100644 --- a/util/src/main/java/net/automatalib/util/graph/Graphs.java +++ b/util/src/main/java/net/automatalib/util/graph/Graphs.java @@ -94,6 +94,25 @@ public static List toNodeList(List edgeList, Graph graph, N i return result; } + /** + * Computes the shortest paths between all pairs of nodes in a graph, using the Floyd-Warshall dynamic programming + * algorithm. This method assumes no edge weights. + * + * @param graph + * the graph + * @param + * node type + * @param + * edge type + * + * @return the all pairs shortest paths result + * + * @see FloydWarshallAPSP + */ + public static APSPResult findAPSP(Graph graph) { + return findAPSP(graph, e -> 0F); + } + /** * Computes the shortest paths between all pairs of nodes in a graph, using the Floyd-Warshall dynamic programming * algorithm. Note that the result is only correct if the graph contains no cycles with negative edge weight sums. From a7caa82bbc04d8475b42586c6ff2729aa43743de Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Tue, 29 Jul 2025 14:25:20 +0200 Subject: [PATCH 2/9] some more polishing added tests that still fail --- .../KWayStateCoverTestsIterator.java | 56 +++++--- .../KWayTransitionCoverTestsIterator.java | 122 +++++++++++------- .../KWayStateCoverTestsIteratorTest.java | 63 +++++++++ .../KWayTransitionCoverTestsIteratorTest.java | 78 +++++++++++ 4 files changed, 253 insertions(+), 66 deletions(-) create mode 100644 util/src/test/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIteratorTest.java create mode 100644 util/src/test/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIteratorTest.java diff --git a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java index ac5783692..a9e988e28 100644 --- a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java +++ b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java @@ -1,3 +1,18 @@ +/* Copyright (C) 2013-2025 TU Dortmund University + * This file is part of AutomataLib . + * + * 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 net.automatalib.util.automaton.conformance; import java.util.ArrayList; @@ -8,6 +23,7 @@ import java.util.List; import java.util.Random; import java.util.Set; + import net.automatalib.automaton.UniversalDeterministicAutomaton; import net.automatalib.automaton.graph.TransitionEdge; import net.automatalib.common.util.HashUtil; @@ -23,6 +39,8 @@ public class KWayStateCoverTestsIterator> implements Iterator> { + private static final int DEFAULT_RANDOM_WALK_LENGTH = 20; + private final A automaton; private final List alphabet; private final Random random; @@ -37,7 +55,7 @@ public KWayStateCoverTestsIterator(A automaton, Collection inputs) } public KWayStateCoverTestsIterator(A automaton, Collection inputs, Random random) { - this(automaton, inputs, random, 2, 20, CombinationMethod.Permutations); + this(automaton, inputs, random, 2, DEFAULT_RANDOM_WALK_LENGTH, CombinationMethod.PERMUTATIONS); } public KWayStateCoverTestsIterator(A automaton, @@ -76,7 +94,7 @@ static Word getRandomChoices(List alphabet, int count, Rando WordBuilder choices = new WordBuilder<>(count); for (int i = 0; i < count; i++) { - choices.add(alphabet.get(random.nextInt(alphabet.size()))); + choices.append(alphabet.get(random.nextInt(alphabet.size()))); } return choices.toWord(); @@ -87,15 +105,13 @@ static Word getRandomChoices(List alphabet, int count, Rando */ private final class FirstPhaseIterator extends AbstractSimplifiedIterator> { - private int idx = 0; + private int idx; @Override protected boolean calculateNext() { - if (automaton.size() == 1) { - if (idx++ < randomWalkLen) { - super.nextValue = getRandomChoices(alphabet, randomWalkLen, random); - return true; - } + if (automaton.size() == 1 && idx++ < randomWalkLen) { + super.nextValue = getRandomChoices(alphabet, randomWalkLen, random); + return true; } return false; } @@ -111,7 +127,7 @@ private final class SecondPhaseIterator extends AbstractSimplifiedIterator> apsp; - public SecondPhaseIterator() { + SecondPhaseIterator() { List states = new ArrayList<>(automaton.getStates()); Collections.shuffle(states, random); this.combIter = method.getCombinations(states, k); @@ -141,7 +157,7 @@ protected boolean calculateNext() { final WordBuilder pathBuilder = new WordBuilder<>(); for (TransitionEdge e : firstPath) { - pathBuilder.add(e.getInput()); + pathBuilder.append(e.getInput()); } /* @@ -163,17 +179,15 @@ protected boolean calculateNext() { } } - if (!possibleTestCase) { - continue; - } + if (possibleTestCase) { + // Add random walk at the end + for (I p : getRandomChoices(alphabet, randomWalkLen, random)) { + pathBuilder.append(p); + } - // Add random walk at the end - for (I p : getRandomChoices(alphabet, randomWalkLen, random)) { - pathBuilder.append(p); + super.nextValue = pathBuilder.toWord(); + return true; } - - super.nextValue = pathBuilder.toWord(); - return true; } return false; @@ -193,13 +207,13 @@ private APSPResult> getAPSP() { } public enum CombinationMethod { - Combinations { + COMBINATIONS { @Override Iterator> getCombinations(List states, int k) { throw new NoSuchMethodError("TODO"); } }, - Permutations { + PERMUTATIONS { @Override Iterator> getCombinations(List states, int k) { return IterableUtil.allTuples(states, k).iterator(); diff --git a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java index d5b9ab520..ce7851540 100644 --- a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java +++ b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java @@ -1,3 +1,18 @@ +/* Copyright (C) 2013-2025 TU Dortmund University + * This file is part of AutomataLib . + * + * 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 net.automatalib.util.automaton.conformance; import java.util.ArrayList; @@ -11,6 +26,7 @@ import java.util.Objects; import java.util.Random; import java.util.Set; + import net.automatalib.automaton.UniversalDeterministicAutomaton; import net.automatalib.automaton.graph.TransitionEdge; import net.automatalib.common.util.HashUtil; @@ -26,6 +42,12 @@ public class KWayTransitionCoverTestsIterator> implements Iterator> { + private static final int DEFAULT_K = 2; + private static final int DEFAULT_NUM_GENERATED_PATHS = 1_000; + private static final int DEFAULT_MAX_PATH_LENGTH = 50; + private static final int DEFAULT_MAX_NUMBER_OF_STEPS = 0; + private static final int DEFAULT_RANDOM_WALK_LENGTH = 10; + private final A automaton; private final List alphabet; private final Random random; @@ -46,7 +68,16 @@ public KWayTransitionCoverTestsIterator(A automaton, Collection inp } public KWayTransitionCoverTestsIterator(A automaton, Collection inputs, Random random) { - this(automaton, inputs, random, 2, Method.Random, 1000, 50, 0, Optimize.Steps, 10); + this(automaton, + inputs, + random, + DEFAULT_K, + Method.RANDOM, + DEFAULT_NUM_GENERATED_PATHS, + DEFAULT_MAX_PATH_LENGTH, + DEFAULT_MAX_NUMBER_OF_STEPS, + Optimize.STEPS, + DEFAULT_RANDOM_WALK_LENGTH); } public KWayTransitionCoverTestsIterator(A automaton, @@ -71,14 +102,14 @@ public KWayTransitionCoverTestsIterator(A automaton, this.randomWalkLen = randomWalkLen; switch (method) { - case Prefix: - this.iterator = generatePrefixSteps(automaton).iterator(); + case PREFIX: + this.iterator = generatePrefixSteps(automaton); break; - case Random: + case RANDOM: this.iterator = new GreedySetCoverIterator(); break; default: - throw new IllegalArgumentException("Unknown method " + method); + this.iterator = Collections.emptyIterator(); } } @@ -105,10 +136,6 @@ private Set> generateRandomPaths(A hypothesis) { return result; } - /** - * * Creates Path object from provided automaton steps within specified Hypothesis context.** * *@return Newly - * created Path instance corresponding to provided step sequence.** - **/ private Path createPath(A hypothesis, Word steps) { Set> transitions = new HashSet<>(); List> transitionsLog = new ArrayList<>(); @@ -142,53 +169,56 @@ private Path createPath(A hypothesis, Word steps) { transitionsLog); } - public Iterable> generatePrefixSteps(A hypothesis) { + private Iterator> generatePrefixSteps(A hypothesis) { List states = new ArrayList<>(hypothesis.getStates()); Collections.reverse(states); - return () -> new PrefixStepsIterator(states.iterator()); + return new PrefixStepsIterator(states.iterator()); } - /** - * Selects an optimal path from available candidates based on optimization strategy defined. - * - * @return Selected Path object or null if no suitable path is found. - **/ private Path selectOptimalPath(Set> covered, Collection> paths) { Path max = Collections.max(paths, optimize.getPathComparator(covered)); - return max.kWayTransitions.size() != covered.size() ? max : null; - } - - private static List getRandomChoices(List alphabet, int count, Random random) { - List choices = new ArrayList<>(count); - - for (int i = 0; i < count; i++) { - choices.add(alphabet.get(random.nextInt(alphabet.size()))); - } - - return choices; + // require max to provide new transitions + return covered.containsAll(max.kWayTransitions) ? null : max; } public enum Method { - Random, - Prefix + RANDOM, + PREFIX } public enum Optimize { - Steps { + STEPS { @Override Comparator> getPathComparator(Set> covered) { - return Comparator.comparingDouble(p -> ((double) (p.kWayTransitions.size() - covered.size())) / + return Comparator.comparingDouble(p -> ((double) computeSizeOfDiff(p.kWayTransitions, covered)) / p.steps.size()); } }, - Queries { + QUERIES { @Override Comparator> getPathComparator(Set> covered) { - return Comparator.comparingDouble(p -> (p.kWayTransitions.size() - covered.size())); + return Comparator.comparingDouble(p -> computeSizeOfDiff(p.kWayTransitions, covered)); } }; abstract Comparator> getPathComparator(Set> covered); + + /** + * Computes the size of the set difference without actually materializing the difference. + * + * @return size of the difference + */ + private static int computeSizeOfDiff(Set> transitions, + Set> covered) { + int size = transitions.size(); + for (KWayTransition t : covered) { + if (transitions.contains(t)) { + size--; + } + } + + return size; + } } private class GreedySetCoverIterator extends AbstractSimplifiedIterator> { @@ -199,7 +229,7 @@ private class GreedySetCoverIterator extends AbstractSimplifiedIterator> private int stepCount; - public GreedySetCoverIterator() { + GreedySetCoverIterator() { this.paths = generateRandomPaths(automaton); this.paths.addAll(cachedPaths); this.covered = new HashSet<>(); @@ -216,13 +246,15 @@ protected boolean calculateNext() { covered.addAll(path.kWayTransitions); paths.remove(path); stepCount += path.steps.size(); -// result.add(path); + //result.add(path); super.nextValue = path.steps; return true; } if (paths.isEmpty()) { - for (Word generatePrefixStep : generatePrefixSteps(automaton)) { + Iterator> prefixIterator = generatePrefixSteps(automaton); + while (prefixIterator.hasNext()) { + Word generatePrefixStep = prefixIterator.next(); paths.add(createPath(automaton, generatePrefixStep)); } } @@ -240,7 +272,7 @@ private class PrefixStepsIterator extends AbstractTwoLevelIterator, W private final APSPResult> apsp; private final S initial; - public PrefixStepsIterator(Iterator listIterator) { + PrefixStepsIterator(Iterator listIterator) { super(listIterator); this.apsp = Graphs.findAPSP(automaton.transitionGraphView(alphabet)); this.initial = automaton.getInitialState(); @@ -264,8 +296,8 @@ protected Word combine(S state, List steps) { wb.append(edge.getInput()); } - wb.addAll(steps); - wb.addAll(getRandomChoices(alphabet, randomWalkLen, random)); + wb.append(steps); + wb.append(KWayStateCoverTestsIterator.getRandomChoices(alphabet, randomWalkLen, random)); return wb.toWord(); } @@ -277,7 +309,7 @@ private static final class KWayTransition { private final S endState; private final Word steps; - private KWayTransition(S startState, S endState, Word steps) { + KWayTransition(S startState, S endState, Word steps) { this.startState = startState; this.endState = endState; this.steps = steps; @@ -311,11 +343,11 @@ private static final class Path { private final Set> kWayTransitions; private final List> transitionsLog; - private Path(S startState, - S endState, - Word steps, - Set> kWayTransitions, - List> transitionsLog) { + Path(S startState, + S endState, + Word steps, + Set> kWayTransitions, + List> transitionsLog) { this.startState = startState; this.endState = endState; this.steps = steps; diff --git a/util/src/test/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIteratorTest.java b/util/src/test/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIteratorTest.java new file mode 100644 index 000000000..fcb7c2c54 --- /dev/null +++ b/util/src/test/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIteratorTest.java @@ -0,0 +1,63 @@ +/* Copyright (C) 2013-2025 TU Dortmund University + * This file is part of AutomataLib . + * + * 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 net.automatalib.util.automaton.conformance; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; + +import net.automatalib.alphabet.Alphabet; +import net.automatalib.alphabet.impl.Alphabets; +import net.automatalib.automaton.UniversalDeterministicAutomaton; +import net.automatalib.automaton.fsa.impl.CompactDFA; +import net.automatalib.common.util.collection.IteratorUtil; +import net.automatalib.util.automaton.random.RandomAutomata; +import net.automatalib.word.Word; +import org.testng.Assert; +import org.testng.annotations.Test; + +@Test +public class KWayStateCoverTestsIteratorTest { + + @Test + public void testDefault() { + + Random random = new Random(42); + Alphabet alphabet = Alphabets.integers(0, 9); + CompactDFA dfa = RandomAutomata.randomDFA(random, 10, alphabet); + + KWayStateCoverTestsIterator iter = new KWayStateCoverTestsIterator<>(dfa, alphabet, random); + + List> tests = IteratorUtil.list(iter); + + assertStateCoverage(dfa, tests); + + } + + private void assertStateCoverage(UniversalDeterministicAutomaton automaton, + Collection> tests) { + + Set cache = new HashSet<>(); + + for (Word test : tests) { + cache.add(automaton.getState(test)); + } + + Assert.assertEquals(cache.size(), automaton.size()); + } +} diff --git a/util/src/test/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIteratorTest.java b/util/src/test/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIteratorTest.java new file mode 100644 index 000000000..d187bc918 --- /dev/null +++ b/util/src/test/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIteratorTest.java @@ -0,0 +1,78 @@ +/* Copyright (C) 2013-2025 TU Dortmund University + * This file is part of AutomataLib . + * + * 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 net.automatalib.util.automaton.conformance; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Random; +import java.util.Set; + +import net.automatalib.alphabet.Alphabet; +import net.automatalib.alphabet.impl.Alphabets; +import net.automatalib.automaton.UniversalDeterministicAutomaton; +import net.automatalib.automaton.fsa.impl.CompactDFA; +import net.automatalib.common.util.collection.IteratorUtil; +import net.automatalib.common.util.mapping.MutableMapping; +import net.automatalib.util.automaton.random.RandomAutomata; +import net.automatalib.word.Word; +import org.testng.Assert; +import org.testng.annotations.Test; + +@Test +public class KWayTransitionCoverTestsIteratorTest { + + @Test + public void testDefault() { + + Random random = new Random(42); + Alphabet alphabet = Alphabets.integers(0, 9); + CompactDFA dfa = RandomAutomata.randomDFA(random, 10, alphabet); + + KWayTransitionCoverTestsIterator iter = + new KWayTransitionCoverTestsIterator<>(dfa, alphabet, random); + + List> tests = IteratorUtil.list(iter); + + assertTransitionCoverage(dfa, alphabet, tests); + + } + + private void assertTransitionCoverage(UniversalDeterministicAutomaton automaton, + Collection inputs, + Collection> tests) { + + MutableMapping> mapping = automaton.createStaticStateMapping(); + + for (S s : automaton) { + mapping.put(s, new HashSet<>()); + } + + for (Word test : tests) { + S state = automaton.getState(test.prefix(-1)); + T t = automaton.getTransition(state, test.lastSymbol()); + mapping.get(state).add(t); + } + + for (S s : automaton) { + Set transitions = mapping.get(s); + Assert.assertNotNull(transitions); + Assert.assertEquals(transitions.size(), inputs.size(), Objects.toString(s)); + } + + } +} From 9750a8d984a7d3f4478d83603e508687883d521a Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Thu, 7 Aug 2025 01:00:43 +0200 Subject: [PATCH 3/9] add missing functionality + consolidations --- CHANGELOG.md | 2 + .../common/util/array/ArrayUtil.java | 12 +- .../collection/AllCombinationsIterator.java | 117 ++++++------- .../collection/AllPermutationsIterator.java | 88 ++++++++++ .../collection/CartesianProductIterator.java | 98 +++++++++++ .../util/collection/CollectionUtil.java | 61 +++++++ .../common/util/collection/IterableUtil.java | 20 ++- .../util/collection/CollectionUtilTest.java | 161 ++++++++++++++++++ .../KWayStateCoverTestsIterator.java | 116 ++++++++++--- .../KWayTransitionCoverTestsIterator.java | 161 +++++++++++++----- .../KWayStateCoverTestsIteratorTest.java | 132 ++++++++++++-- .../KWayTransitionCoverTestsIteratorTest.java | 122 +++++++++---- 12 files changed, 912 insertions(+), 178 deletions(-) create mode 100644 commons/util/src/main/java/net/automatalib/common/util/collection/AllPermutationsIterator.java create mode 100644 commons/util/src/main/java/net/automatalib/common/util/collection/CartesianProductIterator.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 14d78f2fa..7f4ab5dca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * Added a new `automata-serialization-mata` module for serializing (explicit) NFAs in the `.mata` format as used by the [mata library](https://github.com/VeriFIT/mata). * `automata-modelchecking-m3c` now supports ARM-based macOS systems. * `automata-modelchecking-m3c` can now be included in jlink images. +* Added `KWay{State,Transition}CoverTestsIterator`s to `automata-util` as a new means for conformance testing. +* Added `CollectionUtil#allCombintationsIterator` and `CollectionUtil#allPermutationsIterator` for computing k-combinations and k-permutations. ### Changed diff --git a/commons/util/src/main/java/net/automatalib/common/util/array/ArrayUtil.java b/commons/util/src/main/java/net/automatalib/common/util/array/ArrayUtil.java index 0ab030c70..086d4cf33 100644 --- a/commons/util/src/main/java/net/automatalib/common/util/array/ArrayUtil.java +++ b/commons/util/src/main/java/net/automatalib/common/util/array/ArrayUtil.java @@ -163,7 +163,17 @@ public static void heapsort(int[] arr, int[] keys) { } } - private static void swap(int[] arr, int i, int j) { + /** + * Swaps the two elements at the given position in-place. + * + * @param arr + * the array + * @param i + * the first index + * @param j + * the second index + */ + public static void swap(int[] arr, int i, int j) { int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; diff --git a/commons/util/src/main/java/net/automatalib/common/util/collection/AllCombinationsIterator.java b/commons/util/src/main/java/net/automatalib/common/util/collection/AllCombinationsIterator.java index 81f5e9b1e..7a149f0c0 100644 --- a/commons/util/src/main/java/net/automatalib/common/util/collection/AllCombinationsIterator.java +++ b/commons/util/src/main/java/net/automatalib/common/util/collection/AllCombinationsIterator.java @@ -15,84 +15,85 @@ */ package net.automatalib.common.util.collection; -import java.util.ArrayList; -import java.util.Iterator; +import java.util.AbstractList; +import java.util.Collection; import java.util.List; -import java.util.NoSuchElementException; /** - * An iterator that iterates over the cartesian product of its given source domains. Each intermediate combination of - * elements is computed lazily. - *

- * Note: Subsequent calls to the {@link #next()} method return a reference to the same list, and only update the - * contents of the list. If you plan to reuse intermediate results, you'll need to explicitly copy them. + * Iterator for computing all k-combinations of a given collection. Implementation is based on https://hmkcode.com/calculate-find-all-possible-combinations-of-an-array-using-java/. * * @param - * type of elements + * element type */ -final class AllCombinationsIterator implements Iterator> { +final class AllCombinationsIterator extends AbstractSimplifiedIterator> { - private final Iterable[] iterables; - private final Iterator[] iterators; - private final List current; - private boolean first = true; - private boolean empty; + private final int[] pointers; + private final int n; + private final int k; - @SuppressWarnings("unchecked") - @SafeVarargs - AllCombinationsIterator(Iterable... iterables) { - this.iterables = iterables; - this.iterators = new Iterator[iterables.length]; - this.current = new ArrayList<>(iterables.length); - for (int i = 0; i < iterators.length; i++) { - Iterator it = iterables[i].iterator(); - if (!it.hasNext()) { - empty = true; - break; - } - this.iterators[i] = it; - this.current.add(it.next()); - } - } + private int r; // index for combination array + private int i; // index for elements array - @Override - public boolean hasNext() { - if (empty) { - return false; + AllCombinationsIterator(Collection elements, int k) { + if (k < 0 || k > elements.size()) { + throw new IllegalArgumentException("k is not within its expected bounds of 0 and " + elements.size()); } - for (Iterator it : iterators) { - if (it.hasNext()) { - return true; - } - } + this.n = elements.size(); + this.k = k; + + // we only read this array + @SuppressWarnings({"unchecked", "PMD.ClassCastExceptionWithToArray"}) + final T[] pool = (T[]) elements.toArray(); + this.pointers = new int[k]; - return first; + // always use same instance which is modified in-place + super.nextValue = new MappedList<>(pool, this.pointers, this.k); } @Override - public List next() { - if (empty) { - throw new NoSuchElementException(); - } else if (first) { - first = false; - return current; + protected boolean calculateNext() { + while (r >= 0) { + if (i <= n + r - k) { // forward step if i < (n + (r-K)) + pointers[r] = i; + if (r == k - 1) { // if combination array is full print and increment i; + i++; + return true; + } else { // if combination is not full yet, select next element + i = pointers[r] + 1; + r++; + } + } else { // backward step + r--; + if (r >= 0) { + i = pointers[r] + 1; + } + } } + return false; + } - for (int i = 0; i < iterators.length; i++) { - Iterator it = iterators[i]; + static class MappedList extends AbstractList { - if (iterators[i].hasNext()) { - current.set(i, it.next()); - return current; - } + private final T[] pool; + private final int[] pointers; + private final int k; - it = iterables[i].iterator(); - iterators[i] = it; - current.set(i, it.next()); + MappedList(T[] pool, int[] pointers, int k) { + this.pool = pool; + this.pointers = pointers; + this.k = k; } - throw new NoSuchElementException(); - } + @Override + public T get(int index) { + return pool[pointers[index]]; + } + @Override + public int size() { + return k; + } + } } diff --git a/commons/util/src/main/java/net/automatalib/common/util/collection/AllPermutationsIterator.java b/commons/util/src/main/java/net/automatalib/common/util/collection/AllPermutationsIterator.java new file mode 100644 index 000000000..b322bd421 --- /dev/null +++ b/commons/util/src/main/java/net/automatalib/common/util/collection/AllPermutationsIterator.java @@ -0,0 +1,88 @@ +/* Copyright (C) 2013-2025 TU Dortmund University + * This file is part of AutomataLib . + * + * 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 net.automatalib.common.util.collection; + +import java.util.Collection; +import java.util.List; + +import net.automatalib.common.util.array.ArrayUtil; +import net.automatalib.common.util.collection.AllCombinationsIterator.MappedList; + +/** + * Iterator for computing all k-permutations of a given collection. Implementation is based on itertools.permutations. + * + * @param + * element type + */ +final class AllPermutationsIterator extends AbstractSimplifiedIterator> { + + private final int n; + private final int[] indices; + private final int[] cycles; + private final int r; + + private boolean initial; + + AllPermutationsIterator(Collection elements, int k) { + if (k < 0 || k > elements.size()) { + throw new IllegalArgumentException("k is not within its expected bounds of 0 and " + elements.size()); + } + + this.n = elements.size(); + this.r = k; + + // we only read this array + @SuppressWarnings({"unchecked", "PMD.ClassCastExceptionWithToArray"}) + final T[] pool = (T[]) elements.toArray(); + this.indices = new int[this.n]; + this.cycles = new int[k]; + + for (int j = 0; j < n; j++) { + indices[j] = j; + if (j < k) { + cycles[j] = n - j; + } + } + + // always use same instance which is modified in-place + super.nextValue = new MappedList<>(pool, this.indices, k); + } + + @Override + protected boolean calculateNext() { + if (!initial) { // the first element is the unaltered one + initial = true; + return true; + } + + for (int i = r - 1; i >= 0; i--) { + cycles[i] -= 1; + if (cycles[i] == 0) { + int old = indices[i]; + System.arraycopy(indices, i + 1, indices, i, n - i - 1); + indices[n - 1] = old; + cycles[i] = n - i; + } else { + int j = cycles[i]; + ArrayUtil.swap(indices, i, n - j); + return true; + } + } + return false; + } + +} diff --git a/commons/util/src/main/java/net/automatalib/common/util/collection/CartesianProductIterator.java b/commons/util/src/main/java/net/automatalib/common/util/collection/CartesianProductIterator.java new file mode 100644 index 000000000..07053506a --- /dev/null +++ b/commons/util/src/main/java/net/automatalib/common/util/collection/CartesianProductIterator.java @@ -0,0 +1,98 @@ +/* Copyright (C) 2013-2025 TU Dortmund University + * This file is part of AutomataLib . + * + * 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 net.automatalib.common.util.collection; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * An iterator that iterates over the cartesian product of its given source domains. Each intermediate combination of + * elements is computed lazily. + *

+ * Note: Subsequent calls to the {@link #next()} method return a reference to the same list, and only update the + * contents of the list. If you plan to reuse intermediate results, you'll need to explicitly copy them. + * + * @param + * type of elements + */ +final class CartesianProductIterator implements Iterator> { + + private final Iterable[] iterables; + private final Iterator[] iterators; + private final List current; + private boolean first = true; + private boolean empty; + + @SuppressWarnings("unchecked") + @SafeVarargs + CartesianProductIterator(Iterable... iterables) { + this.iterables = iterables; + this.iterators = new Iterator[iterables.length]; + this.current = new ArrayList<>(iterables.length); + for (int i = 0; i < iterators.length; i++) { + Iterator it = iterables[i].iterator(); + if (!it.hasNext()) { + empty = true; + break; + } + this.iterators[i] = it; + this.current.add(it.next()); + } + } + + @Override + public boolean hasNext() { + if (empty) { + return false; + } + + for (Iterator it : iterators) { + if (it.hasNext()) { + return true; + } + } + + return first; + } + + @Override + public List next() { + if (empty) { + throw new NoSuchElementException(); + } else if (first) { + first = false; + return current; + } + + for (int i = 0; i < iterators.length; i++) { + Iterator it = iterators[i]; + + if (iterators[i].hasNext()) { + current.set(i, it.next()); + return current; + } + + it = iterables[i].iterator(); + iterators[i] = it; + current.set(i, it.next()); + } + + throw new NoSuchElementException(); + } + +} diff --git a/commons/util/src/main/java/net/automatalib/common/util/collection/CollectionUtil.java b/commons/util/src/main/java/net/automatalib/common/util/collection/CollectionUtil.java index 892ed103f..8e9b9af44 100644 --- a/commons/util/src/main/java/net/automatalib/common/util/collection/CollectionUtil.java +++ b/commons/util/src/main/java/net/automatalib/common/util/collection/CollectionUtil.java @@ -55,6 +55,67 @@ public static List randomAccessList(Collection coll) { return new ArrayList<>(coll); } + /** + * Return {@code k}-length subsequences of elements from the input collection. + *

+ * Note: + * Elements are treated as unique based on their position, not on their value. If the input elements are unique, + * there will be no repeated values within each combination. Subsequent calls to the returned iterator's + * {@link Iterator#next() next()} method return a reference to the same list, and only update the contents of the + * list. If you plan to reuse intermediate results, you'll need to explicitly copy them. + * + * @param elements + * the collection for the source elements + * @param k + * the length of the (partial) combination + * @param + * type of elements + * + * @return an iterator iterating over all k-combinations + */ + public static Iterator> allCombintationsIterator(Collection elements, int k) { + return new AllCombinationsIterator<>(elements, k); + } + + /** + * Convenience method for {@link #allPermutationsIterator(Collection, int)} which uses the number of elements as + * {@code k}. + * + * @param elements + * the collection for the source elements + * @param + * type of elements + * + * @return an iterator iterating over all k-permutations + * + * @see #allPermutationsIterator(Collection, int) + */ + public static Iterator> allPermutationsIterator(Collection elements) { + return allPermutationsIterator(elements, elements.size()); + } + + /** + * Return {@code k}-length permutations of elements from the input collection. + *

+ * Note: + * Elements are treated as unique based on their position, not on their value. If the input elements are unique, + * there will be no repeated values within each permutation. Subsequent calls to the returned iterator's + * {@link Iterator#next() next()} method return a reference to the same list, and only update the contents of the + * list. If you plan to reuse intermediate results, you'll need to explicitly copy them. + * + * @param elements + * the collection for the source elements + * @param k + * the length of the (partial) permutation + * @param + * type of elements + * + * @return an iterator iterating over all k-permutations + */ + public static Iterator> allPermutationsIterator(Collection elements, int k) { + return new AllPermutationsIterator<>(elements, k); + } + /** * Returns a (mutable) view on the given collection that transforms its elements as specified by the given mapping. * diff --git a/commons/util/src/main/java/net/automatalib/common/util/collection/IterableUtil.java b/commons/util/src/main/java/net/automatalib/common/util/collection/IterableUtil.java index 5a7d93d30..23380fa78 100644 --- a/commons/util/src/main/java/net/automatalib/common/util/collection/IterableUtil.java +++ b/commons/util/src/main/java/net/automatalib/common/util/collection/IterableUtil.java @@ -31,6 +31,22 @@ private IterableUtil() { // prevent instantiation } + /** + * Convenience method for {@link #allTuples(Iterable, int, int)} that uses {@code length} for both {@code minLength} + * and {@code maxLength}. + * + * @param domain + * the iterables for the source domain + * @param length + * the minimal length of the tuple + * @param + * type of elements + * + * @return an iterator that iterates over all tuples of the given source domain whose length (dimension) is the + * given parameter + * + * @see #allTuples(Iterable, int, int) + */ public static Iterable> allTuples(Iterable domain, int length) { return allTuples(domain, length, length); } @@ -44,7 +60,7 @@ public static Iterable> allTuples(Iterable domain, int * you'll need to explicitly copy them. * * @param domain - * the iterables for the source domains + * the iterables for the source domain * @param minLength * the minimal length of the tuple * @param maxLength @@ -90,7 +106,7 @@ public static Iterable> cartesianProduct(Iterable... iterables) { return Collections.singletonList(Collections.emptyList()); } - return () -> new AllCombinationsIterator<>(iterables); + return () -> new CartesianProductIterator<>(iterables); } /** diff --git a/commons/util/src/test/java/net/automatalib/common/util/collection/CollectionUtilTest.java b/commons/util/src/test/java/net/automatalib/common/util/collection/CollectionUtilTest.java index 7bccee55c..753fa00ec 100644 --- a/commons/util/src/test/java/net/automatalib/common/util/collection/CollectionUtilTest.java +++ b/commons/util/src/test/java/net/automatalib/common/util/collection/CollectionUtilTest.java @@ -15,7 +15,12 @@ */ package net.automatalib.common.util.collection; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; +import java.util.Iterator; +import java.util.List; import java.util.Set; import org.testng.Assert; @@ -23,6 +28,8 @@ public class CollectionUtilTest { + private static final List COLLECTION = Arrays.asList('A', 'B', 'C', 'D'); + @Test public void testAdd() { final Set set = new HashSet<>(); @@ -33,4 +40,158 @@ public void testAdd() { Assert.assertFalse(CollectionUtil.add(set, elements.iterator())); Assert.assertEquals(set, elements); } + + @Test + public void testIllegalPermutations() { + Assert.assertThrows(IllegalArgumentException.class, + () -> CollectionUtil.allPermutationsIterator(COLLECTION, -1)); + Assert.assertThrows(IllegalArgumentException.class, + () -> CollectionUtil.allPermutationsIterator(COLLECTION, 5)); + } + + @Test + public void testAllTwoPermutations() { + final int k = 2; + final Iterator> lists = CollectionUtil.allPermutationsIterator(COLLECTION, k); + final List tuples = new ArrayList<>(); + + while (lists.hasNext()) { + StringBuilder sb = new StringBuilder(k); + lists.next().forEach(sb::append); + Assert.assertTrue(tuples.add(sb.toString())); + } + + Assert.assertEquals(tuples, + Arrays.asList("AB", "AC", "AD", "BA", "BC", "BD", "CA", "CB", "CD", "DA", "DB", "DC")); + } + + @Test + public void testAllThreePermutations() { + final int k = 3; + final Iterator> lists = CollectionUtil.allPermutationsIterator(COLLECTION, k); + final List tuples = new ArrayList<>(); + + while (lists.hasNext()) { + StringBuilder sb = new StringBuilder(k); + lists.next().forEach(sb::append); + Assert.assertTrue(tuples.add(sb.toString())); + } + + Assert.assertEquals(tuples, + Arrays.asList("ABC", + "ABD", + "ACB", + "ACD", + "ADB", + "ADC", + "BAC", + "BAD", + "BCA", + "BCD", + "BDA", + "BDC", + "CAB", + "CAD", + "CBA", + "CBD", + "CDA", + "CDB", + "DAB", + "DAC", + "DBA", + "DBC", + "DCA", + "DCB")); + } + + @Test + public void testAllFourPermutations() { + final int n = COLLECTION.size(); + final Iterator> lists = CollectionUtil.allPermutationsIterator(COLLECTION); + final List tuples = new ArrayList<>(); + + while (lists.hasNext()) { + StringBuilder sb = new StringBuilder(n); + lists.next().forEach(sb::append); + Assert.assertTrue(tuples.add(sb.toString())); + } + + final List expected = new ArrayList<>(); + // brute-force 4 dimensional tuples + for (int i1 = 0; i1 < n; i1++) { + for (int i2 = 0; i2 < n; i2++) { + if (i1 != i2) { + for (int i3 = 0; i3 < n; i3++) { + if (i3 != i1 && i3 != i2) { + for (int i4 = 0; i4 < n; i4++) { + if (i4 != i1 && i4 != i2 && i4 != i3) { + final StringBuilder sb = new StringBuilder(); + sb.append(COLLECTION.get(i1)) + .append(COLLECTION.get(i2)) + .append(COLLECTION.get(i3)) + .append(COLLECTION.get(i4)); + expected.add(sb.toString()); + } + } + } + } + } + } + } + + Assert.assertEquals(tuples, expected); + } + + @Test + public void testIllegalCombinations() { + Assert.assertThrows(IllegalArgumentException.class, + () -> CollectionUtil.allCombintationsIterator(COLLECTION, -1)); + Assert.assertThrows(IllegalArgumentException.class, + () -> CollectionUtil.allCombintationsIterator(COLLECTION, 5)); + } + + @Test + public void testAllTwoCombinations() { + final int k = 2; + final Iterator> lists = CollectionUtil.allCombintationsIterator(COLLECTION, k); + final List tuples = new ArrayList<>(); + + while (lists.hasNext()) { + StringBuilder sb = new StringBuilder(k); + lists.next().forEach(sb::append); + Assert.assertTrue(tuples.add(sb.toString())); + } + + Assert.assertEquals(tuples, Arrays.asList("AB", "AC", "AD", "BC", "BD", "CD")); + } + + @Test + public void testAllThreeCombinations() { + final int k = 3; + final Iterator> lists = CollectionUtil.allCombintationsIterator(COLLECTION, k); + final List tuples = new ArrayList<>(); + + while (lists.hasNext()) { + StringBuilder sb = new StringBuilder(k); + lists.next().forEach(sb::append); + Assert.assertTrue(tuples.add(sb.toString())); + } + + Assert.assertEquals(tuples, Arrays.asList("ABC", "ABD", "ACD", "BCD")); + } + + @Test + public void testAllFourCombinations() { + final int k = 4; + final Iterator> lists = CollectionUtil.allCombintationsIterator(COLLECTION, k); + final List tuples = new ArrayList<>(); + + while (lists.hasNext()) { + StringBuilder sb = new StringBuilder(k); + lists.next().forEach(sb::append); + Assert.assertTrue(tuples.add(sb.toString())); + } + + Assert.assertEquals(tuples, Collections.singleton("ABCD")); + } } diff --git a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java index a9e988e28..bca69ebbc 100644 --- a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java +++ b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java @@ -29,17 +29,35 @@ import net.automatalib.common.util.HashUtil; import net.automatalib.common.util.collection.AbstractSimplifiedIterator; import net.automatalib.common.util.collection.CollectionUtil; -import net.automatalib.common.util.collection.IterableUtil; import net.automatalib.common.util.collection.IteratorUtil; +import net.automatalib.common.util.random.RandomUtil; import net.automatalib.util.graph.Graphs; import net.automatalib.util.graph.apsp.APSPResult; import net.automatalib.word.Word; import net.automatalib.word.WordBuilder; +/** + * A randomized state cover test generator 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. + * + * @param + * automaton state type + * @param + * input symbol type + * @param + * transition type + * @param + * automaton type + */ public class KWayStateCoverTestsIterator> implements Iterator> { - private static final int DEFAULT_RANDOM_WALK_LENGTH = 20; + public static final int DEFAULT_R_WALK_LEN = 20; + public static final int DEFAULT_K = 2; private final A automaton; private final List alphabet; @@ -50,32 +68,74 @@ public class KWayStateCoverTestsIterator> iterator; + /** + * Convenience constructor which uses a fresh {@code random} object. + * + * @param automaton + * the automaton for which to generate test cases + * @param inputs + * the inputs to consider for test case generation + * + * @see #KWayStateCoverTestsIterator(UniversalDeterministicAutomaton, Collection, Random) + */ public KWayStateCoverTestsIterator(A automaton, Collection inputs) { this(automaton, inputs, new Random()); } + /** + * Convenience constructor. Uses {@code k=2}, {@code randomWalkLen = 20}, and + * {@code method = CombinationMethod.PERMUTATIONS}. + * + * @param automaton + * the automaton for which to generate test cases + * @param inputs + * the inputs to consider for test case generation + * @param random + * the random number generator to use + * + * @see #KWayStateCoverTestsIterator(UniversalDeterministicAutomaton, Collection, Random, int, int, + * CombinationMethod) + */ public KWayStateCoverTestsIterator(A automaton, Collection inputs, Random random) { - this(automaton, inputs, random, 2, DEFAULT_RANDOM_WALK_LENGTH, CombinationMethod.PERMUTATIONS); + this(automaton, inputs, random, DEFAULT_R_WALK_LEN, DEFAULT_K, CombinationMethod.PERMUTATIONS); } + /** + * Constructor. + * + * @param automaton + * the automaton for which to generate test cases + * @param inputs + * the inputs to consider for test case generation + * @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 method + * the method for computing combinations + */ public KWayStateCoverTestsIterator(A automaton, Collection inputs, Random random, - int k, int randomWalkLen, + int k, CombinationMethod method) { this.automaton = automaton; this.alphabet = CollectionUtil.randomAccessList(inputs); - this.k = k; + this.k = Math.min(k, automaton.size()); this.randomWalkLen = randomWalkLen; this.method = method; this.random = random; - if (automaton.size() == 0) { + final S initial = automaton.getInitialState(); + + if (automaton.size() == 0 || initial == null) { this.iterator = Collections.emptyIterator(); } else { final FirstPhaseIterator firstIterator = new FirstPhaseIterator(); - final SecondPhaseIterator secondPhaseIterator = new SecondPhaseIterator(); + final SecondPhaseIterator secondPhaseIterator = new SecondPhaseIterator(initial); this.iterator = IteratorUtil.concat(firstIterator, secondPhaseIterator); } } @@ -90,16 +150,6 @@ public Word next() { return iterator.next(); } - static Word getRandomChoices(List alphabet, int count, Random random) { - WordBuilder choices = new WordBuilder<>(count); - - for (int i = 0; i < count; i++) { - choices.append(alphabet.get(random.nextInt(alphabet.size()))); - } - - return choices.toWord(); - } - /** * Performs random walks if the automaton only has a single state. */ @@ -110,7 +160,7 @@ private final class FirstPhaseIterator extends AbstractSimplifiedIterator> combIter; private final Set>>> cache; + private final S initial; private APSPResult> apsp; - SecondPhaseIterator() { + SecondPhaseIterator(S initial) { + this.initial = initial; List states = new ArrayList<>(automaton.getStates()); Collections.shuffle(states, random); this.combIter = method.getCombinations(states, k); @@ -137,7 +189,6 @@ private final class SecondPhaseIterator extends AbstractSimplifiedIterator> apsp = getAPSP(); while (combIter.hasNext()) { @@ -166,7 +217,7 @@ protected boolean calculateNext() { */ boolean possibleTestCase = true; for (int index = 0; index < comb.size() - 1; index++) { - final List> pathBetweenStates = + final List> pathBetweenStates = apsp.getShortestPath(comb.get(index), comb.get(index + 1)); if (pathBetweenStates == null || pathBetweenStates.isEmpty()) { @@ -180,11 +231,7 @@ protected boolean calculateNext() { } if (possibleTestCase) { - // Add random walk at the end - for (I p : getRandomChoices(alphabet, randomWalkLen, random)) { - pathBuilder.append(p); - } - + pathBuilder.append(RandomUtil.sample(random, alphabet, randomWalkLen)); super.nextValue = pathBuilder.toWord(); return true; } @@ -206,17 +253,30 @@ private APSPResult> getAPSP() { } } + /** + * The specific method for generating combinations of states during exploration. + */ public enum CombinationMethod { + /** + * Generate all k-combinations of states. + * + * @see CollectionUtil#allCombintationsIterator(Collection, int) + */ COMBINATIONS { @Override Iterator> getCombinations(List states, int k) { - throw new NoSuchMethodError("TODO"); + return CollectionUtil.allCombintationsIterator(states, k); } }, + /** + * Generate all k-permutations of states. + * + * @see CollectionUtil#allPermutationsIterator(Collection, int) + */ PERMUTATIONS { @Override Iterator> getCombinations(List states, int k) { - return IterableUtil.allTuples(states, k).iterator(); + return CollectionUtil.allPermutationsIterator(states, k); } }; diff --git a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java index ce7851540..0380bc84d 100644 --- a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java +++ b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java @@ -34,19 +34,38 @@ import net.automatalib.common.util.collection.AbstractTwoLevelIterator; import net.automatalib.common.util.collection.CollectionUtil; import net.automatalib.common.util.collection.IterableUtil; +import net.automatalib.common.util.random.RandomUtil; import net.automatalib.util.graph.Graphs; import net.automatalib.util.graph.apsp.APSPResult; import net.automatalib.word.Word; import net.automatalib.word.WordBuilder; +/** + * A randomized state cover test generator 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 iterates selects test cases based on k-way transitions coverage. It does that by generating random test words + * and finding the smallest subset with the highest coverage. In other words, this iterator generates test words by + * running random paths that cover all pairwise / k-way transitions. + * + * @param + * automaton state type + * @param + * input symbol type + * @param + * transition type + * @param + * automaton type + */ public class KWayTransitionCoverTestsIterator> implements Iterator> { - private static final int DEFAULT_K = 2; - private static final int DEFAULT_NUM_GENERATED_PATHS = 1_000; - private static final int DEFAULT_MAX_PATH_LENGTH = 50; - private static final int DEFAULT_MAX_NUMBER_OF_STEPS = 0; - private static final int DEFAULT_RANDOM_WALK_LENGTH = 10; + public static final int DEFAULT_R_WALK_LEN = 10; + public static final int DEFAULT_NUM_GEN_PATHS = 1_000; + public static final int DEFAULT_MAX_PATH_LENGTH = 50; + public static final int DEFAULT_MAX_NUM_STEPS = 0; + public static final int DEFAULT_K = 2; private final A automaton; private final List alphabet; @@ -55,61 +74,105 @@ public class KWayTransitionCoverTestsIterator> cachedPaths = new ArrayList<>(); - private final Iterator> iterator; + /** + * Convenience constructor which uses a fresh {@code random} object. + * + * @param automaton + * the automaton for which to generate test cases + * @param inputs + * the inputs to consider for test case generation + * + * @see #KWayTransitionCoverTestsIterator(UniversalDeterministicAutomaton, Collection, Random) + */ public KWayTransitionCoverTestsIterator(A automaton, Collection inputs) { this(automaton, inputs, new Random()); } + /** + * Convenience constructor. Uses {@code randomWalkLen=10}, {@code numGeneratePaths = 1_000}, + * {@code maxPathLen = 50}, {@code maxNumberOfSteps = 0}, {@code k = 2}, + * {@code generationMethod = GenerationMethod.RANDOM}, and {@code optimizationMetric = OptimizationMetric.STEPS}. + * + * @param automaton + * the automaton for which to generate test cases + * @param inputs + * the inputs to consider for test case generation + * @param random + * the random number generator to use + * + * @see #KWayTransitionCoverTestsIterator(UniversalDeterministicAutomaton, Collection, Random, int, int, int, int, + * int, GenerationMethod, OptimizationMetric) + */ public KWayTransitionCoverTestsIterator(A automaton, Collection inputs, Random random) { this(automaton, inputs, random, - DEFAULT_K, - Method.RANDOM, - DEFAULT_NUM_GENERATED_PATHS, + DEFAULT_R_WALK_LEN, + DEFAULT_NUM_GEN_PATHS, DEFAULT_MAX_PATH_LENGTH, - DEFAULT_MAX_NUMBER_OF_STEPS, - Optimize.STEPS, - DEFAULT_RANDOM_WALK_LENGTH); + DEFAULT_MAX_NUM_STEPS, + DEFAULT_K, + GenerationMethod.RANDOM, + OptimizationMetric.STEPS); } + /** + * Constructor. + * + * @param automaton + * the automaton for which to generate test cases + * @param inputs + * the inputs to consider for test case generation + * @param random + * the random number generator to use + * @param randomWalkLen + * the number of steps that are added by 'prefix' generated paths + * @param numGeneratePaths + * number of random queries used to find the optimal subset + * @param maxPathLen + * the maximum step size of a generated path + * @param maxNumberOfSteps + * maximum number of steps that will be executed on the automaton (<=0 = 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 generationMethod + * defines how the queries are generated 'random' or 'prefix' + * @param optimizationMetric + * minimize either the number of 'steps' or 'queries' that are executed + */ public KWayTransitionCoverTestsIterator(A automaton, Collection inputs, Random random, - int k, - Method method, + int randomWalkLen, int numGeneratePaths, int maxPathLen, int maxNumberOfSteps, - Optimize optimize, - int randomWalkLen) { + int k, + GenerationMethod generationMethod, + OptimizationMetric optimizationMetric) { this.automaton = automaton; this.alphabet = CollectionUtil.randomAccessList(inputs); this.random = random; - this.k = k; + this.k = Math.min(k, automaton.size()); this.numGeneratePaths = numGeneratePaths; this.maxPathLen = maxPathLen; this.maxNumberOfSteps = maxNumberOfSteps; - this.optimize = optimize; + this.optimizationMetric = optimizationMetric; this.randomWalkLen = randomWalkLen; - switch (method) { - case PREFIX: - this.iterator = generatePrefixSteps(automaton); - break; - case RANDOM: - this.iterator = new GreedySetCoverIterator(); - break; - default: - this.iterator = Collections.emptyIterator(); + final S initial = automaton.getInitialState(); + + if (automaton.size() == 0 || initial == null) { + this.iterator = Collections.emptyIterator(); + } else { + this.iterator = generationMethod.getIterator(this); } } @@ -128,7 +191,7 @@ private Set> generateRandomPaths(A hypothesis) { for (int i = 0; i < numGeneratePaths; i++) { int randomLength = random.nextInt(maxPathLen - k + 1) + k; // Ensuring length is at least `k` - Word steps = KWayStateCoverTestsIterator.getRandomChoices(alphabet, randomLength, random); + Word steps = Word.fromList(RandomUtil.sample(random, alphabet, randomLength)); Path path = createPath(hypothesis, steps); result.add(path); } @@ -176,17 +239,31 @@ private Iterator> generatePrefixSteps(A hypothesis) { } private Path selectOptimalPath(Set> covered, Collection> paths) { - Path max = Collections.max(paths, optimize.getPathComparator(covered)); - // require max to provide new transitions - return covered.containsAll(max.kWayTransitions) ? null : max; + Path max = Collections.max(paths, optimizationMetric.getPathComparator(covered)); + return max.kWayTransitions.size() == covered.size() ? null : max; } - public enum Method { - RANDOM, - PREFIX + public enum GenerationMethod { + RANDOM { + @Override + > Iterator> getIterator( + KWayTransitionCoverTestsIterator self) { + return self.new GreedySetCoverIterator(); + } + }, + PREFIX { + @Override + > Iterator> getIterator( + KWayTransitionCoverTestsIterator self) { + return self.generatePrefixSteps(self.automaton); + } + }; + + abstract > Iterator> getIterator( + KWayTransitionCoverTestsIterator self); } - public enum Optimize { + public enum OptimizationMetric { STEPS { @Override Comparator> getPathComparator(Set> covered) { @@ -197,7 +274,7 @@ Comparator> getPathComparator(Set> covere QUERIES { @Override Comparator> getPathComparator(Set> covered) { - return Comparator.comparingDouble(p -> computeSizeOfDiff(p.kWayTransitions, covered)); + return Comparator.comparingDouble(p -> p.kWayTransitions.size() - covered.size()); } }; @@ -231,7 +308,6 @@ private class GreedySetCoverIterator extends AbstractSimplifiedIterator> GreedySetCoverIterator() { this.paths = generateRandomPaths(automaton); - this.paths.addAll(cachedPaths); this.covered = new HashSet<>(); this.stepCount = 0; this.sizeOfUniverse = automaton.getStates().size() * (int) Math.pow(alphabet.size(), k); @@ -246,7 +322,6 @@ protected boolean calculateNext() { covered.addAll(path.kWayTransitions); paths.remove(path); stepCount += path.steps.size(); - //result.add(path); super.nextValue = path.steps; return true; } @@ -296,8 +371,8 @@ protected Word combine(S state, List steps) { wb.append(edge.getInput()); } - wb.append(steps); - wb.append(KWayStateCoverTestsIterator.getRandomChoices(alphabet, randomWalkLen, random)); + wb.addAll(steps); + wb.addAll(RandomUtil.sample(random, alphabet, randomWalkLen)); return wb.toWord(); } diff --git a/util/src/test/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIteratorTest.java b/util/src/test/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIteratorTest.java index fcb7c2c54..cf48f095e 100644 --- a/util/src/test/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIteratorTest.java +++ b/util/src/test/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIteratorTest.java @@ -15,7 +15,6 @@ */ package net.automatalib.util.automaton.conformance; -import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Random; @@ -25,39 +24,144 @@ import net.automatalib.alphabet.impl.Alphabets; import net.automatalib.automaton.UniversalDeterministicAutomaton; import net.automatalib.automaton.fsa.impl.CompactDFA; +import net.automatalib.automaton.transducer.impl.CompactMealy; +import net.automatalib.common.util.HashUtil; import net.automatalib.common.util.collection.IteratorUtil; +import net.automatalib.util.automaton.conformance.KWayStateCoverTestsIterator.CombinationMethod; import net.automatalib.util.automaton.random.RandomAutomata; import net.automatalib.word.Word; import org.testng.Assert; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; @Test public class KWayStateCoverTestsIteratorTest { + private static final Alphabet ALPHABET = Alphabets.characters('a', 'c'); + + @DataProvider(name = "methods") + public static Object[][] getMethods() { + return new Object[][] {{CombinationMethod.COMBINATIONS}, {CombinationMethod.PERMUTATIONS}}; + } + @Test - public void testDefault() { + public void testEmptyAutomaton() { + final CompactDFA dfa = new CompactDFA<>(ALPHABET); + final List> tests = IteratorUtil.list(new KWayStateCoverTestsIterator<>(dfa, ALPHABET)); + + Assert.assertTrue(tests.isEmpty()); + } + + @Test(dataProvider = "methods") + public void testSingleStateAutomaton(CombinationMethod method) { + final CompactDFA dfa = new CompactDFA<>(ALPHABET); + + final int initial = dfa.addIntInitialState(); + for (int i = 0; i < ALPHABET.size(); i++) { + dfa.setTransition(initial, i, initial); + } + + final int length = KWayStateCoverTestsIterator.DEFAULT_R_WALK_LEN; + final List> tests = IteratorUtil.list(new KWayStateCoverTestsIterator<>(dfa, + ALPHABET, + new Random(42), + length, + KWayStateCoverTestsIterator.DEFAULT_K, + method)); + + // check that the first 'length' queries are the randomly generated ones. + Assert.assertTrue(tests.size() >= length); + for (Word t : tests.subList(0, length)) { + Assert.assertEquals(t.size(), length); + } + } + + @Test(dataProvider = "methods") + public void testNoInitialStateAutomaton(CombinationMethod method) { + final CompactDFA dfa = RandomAutomata.randomDFA(new Random(42), 10, ALPHABET); + + dfa.setInitialState(null); + final List> tests = IteratorUtil.list(new KWayStateCoverTestsIterator<>(dfa, + ALPHABET, + new Random(42), + KWayStateCoverTestsIterator.DEFAULT_R_WALK_LEN, + KWayStateCoverTestsIterator.DEFAULT_K, + method)); - Random random = new Random(42); - Alphabet alphabet = Alphabets.integers(0, 9); - CompactDFA dfa = RandomAutomata.randomDFA(random, 10, alphabet); + Assert.assertTrue(tests.isEmpty()); + } + + @Test(dataProvider = "methods") + public void testRandomAutomaton(CombinationMethod method) { + final CompactMealy mealy = + RandomAutomata.randomMealy(new Random(42), 10, ALPHABET, Alphabets.integers(0, 2)); + + final List> tests = IteratorUtil.list(new KWayStateCoverTestsIterator<>(mealy, + ALPHABET, + new Random(42), + KWayStateCoverTestsIterator.DEFAULT_R_WALK_LEN, + KWayStateCoverTestsIterator.DEFAULT_K, + method)); + + verifyEachStateVisited(mealy, tests); + } + + @Test(dataProvider = "methods") + public void testKeylockAutomaton(CombinationMethod method) { + final CompactDFA mealy = generateKeylockAutomaton(ALPHABET); + + final List> tests = IteratorUtil.list(new KWayStateCoverTestsIterator<>(mealy, + ALPHABET, + new Random(42), + KWayStateCoverTestsIterator.DEFAULT_R_WALK_LEN, + KWayStateCoverTestsIterator.DEFAULT_K, + method)); - KWayStateCoverTestsIterator iter = new KWayStateCoverTestsIterator<>(dfa, alphabet, random); + verifyEachStateVisited(mealy, tests); + } + + static void verifyEachStateVisited(UniversalDeterministicAutomaton automaton, + List> tests) { + final Set visited = new HashSet<>(HashUtil.capacity(automaton.size())); - List> tests = IteratorUtil.list(iter); + final S init = automaton.getInitialState(); + Assert.assertNotNull(init); - assertStateCoverage(dfa, tests); + visited.add(init); + + for (Word t : tests) { + S iter = init; + for (I i : t) { + S succ = automaton.getSuccessor(iter, i); + Assert.assertNotNull(succ); + visited.add(succ); + iter = succ; + } + } + Assert.assertEquals(visited, new HashSet<>(automaton.getStates())); } - private void assertStateCoverage(UniversalDeterministicAutomaton automaton, - Collection> tests) { + static CompactDFA generateKeylockAutomaton(Alphabet alphabet) { - Set cache = new HashSet<>(); + final CompactDFA result = new CompactDFA<>(alphabet); + + int iter = result.addIntInitialState(); + + for (int i = 0; i < 10 - 1; i++) { + for (int j = 1; j < alphabet.size(); j++) { + result.setTransition(iter, j, iter); + } + int next = result.addIntState(); + result.setTransition(iter, 0, next); + iter = next; + } - for (Word test : tests) { - cache.add(automaton.getState(test)); + result.setAccepting(iter, true); + for (int i = 0; i < alphabet.size(); i++) { + result.setTransition(iter, i, iter); } - Assert.assertEquals(cache.size(), automaton.size()); + return result; } } diff --git a/util/src/test/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIteratorTest.java b/util/src/test/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIteratorTest.java index d187bc918..d4c7ee535 100644 --- a/util/src/test/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIteratorTest.java +++ b/util/src/test/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIteratorTest.java @@ -15,64 +15,122 @@ */ package net.automatalib.util.automaton.conformance; -import java.util.Collection; -import java.util.HashSet; import java.util.List; -import java.util.Objects; import java.util.Random; -import java.util.Set; import net.automatalib.alphabet.Alphabet; import net.automatalib.alphabet.impl.Alphabets; -import net.automatalib.automaton.UniversalDeterministicAutomaton; import net.automatalib.automaton.fsa.impl.CompactDFA; +import net.automatalib.automaton.transducer.impl.CompactMealy; import net.automatalib.common.util.collection.IteratorUtil; -import net.automatalib.common.util.mapping.MutableMapping; +import net.automatalib.util.automaton.conformance.KWayTransitionCoverTestsIterator.GenerationMethod; +import net.automatalib.util.automaton.conformance.KWayTransitionCoverTestsIterator.OptimizationMetric; import net.automatalib.util.automaton.random.RandomAutomata; import net.automatalib.word.Word; import org.testng.Assert; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; @Test public class KWayTransitionCoverTestsIteratorTest { - @Test - public void testDefault() { + private static final Alphabet ALPHABET = Alphabets.characters('a', 'c'); + private static final int AUTOMATON_SIZE = 10; - Random random = new Random(42); - Alphabet alphabet = Alphabets.integers(0, 9); - CompactDFA dfa = RandomAutomata.randomDFA(random, 10, alphabet); + @DataProvider(name = "config") + public static Object[][] getConfig() { + final Object[][] result = new Object[GenerationMethod.values().length * OptimizationMetric.values().length][]; + int idx = 0; - KWayTransitionCoverTestsIterator iter = - new KWayTransitionCoverTestsIterator<>(dfa, alphabet, random); + for (GenerationMethod method : GenerationMethod.values()) { + for (OptimizationMetric metric : OptimizationMetric.values()) { + result[idx++] = new Object[] {method, metric}; + } + } - List> tests = IteratorUtil.list(iter); + return result; + } - assertTransitionCoverage(dfa, alphabet, tests); + @Test + public void testEmptyAutomaton() { + final CompactDFA dfa = new CompactDFA<>(ALPHABET); + final List> tests = IteratorUtil.list(new KWayTransitionCoverTestsIterator<>(dfa, ALPHABET)); + Assert.assertTrue(tests.isEmpty()); } - private void assertTransitionCoverage(UniversalDeterministicAutomaton automaton, - Collection inputs, - Collection> tests) { + @Test(dataProvider = "config") + public void testSingleStateAutomaton(GenerationMethod method, OptimizationMetric metric) { + final CompactDFA dfa = new CompactDFA<>(ALPHABET); - MutableMapping> mapping = automaton.createStaticStateMapping(); - - for (S s : automaton) { - mapping.put(s, new HashSet<>()); + final int initial = dfa.addIntInitialState(); + for (int i = 0; i < ALPHABET.size(); i++) { + dfa.setTransition(initial, i, initial); } - for (Word test : tests) { - S state = automaton.getState(test.prefix(-1)); - T t = automaton.getTransition(state, test.lastSymbol()); - mapping.get(state).add(t); - } + final List> tests = IteratorUtil.list(new KWayTransitionCoverTestsIterator<>(dfa, + ALPHABET, + new Random(42), + KWayTransitionCoverTestsIterator.DEFAULT_R_WALK_LEN, + KWayTransitionCoverTestsIterator.DEFAULT_NUM_GEN_PATHS, + KWayTransitionCoverTestsIterator.DEFAULT_MAX_PATH_LENGTH, + KWayTransitionCoverTestsIterator.DEFAULT_MAX_NUM_STEPS, + KWayTransitionCoverTestsIterator.DEFAULT_K, + method, + metric)); + KWayStateCoverTestsIteratorTest.verifyEachStateVisited(dfa, tests); + } - for (S s : automaton) { - Set transitions = mapping.get(s); - Assert.assertNotNull(transitions); - Assert.assertEquals(transitions.size(), inputs.size(), Objects.toString(s)); - } + @Test(dataProvider = "config") + public void testNoInitialStateAutomaton(GenerationMethod method, OptimizationMetric metric) { + final CompactDFA dfa = RandomAutomata.randomDFA(new Random(42), AUTOMATON_SIZE, ALPHABET); + + dfa.setInitialState(null); + final List> tests = IteratorUtil.list(new KWayTransitionCoverTestsIterator<>(dfa, + ALPHABET, + new Random(42), + KWayTransitionCoverTestsIterator.DEFAULT_R_WALK_LEN, + KWayTransitionCoverTestsIterator.DEFAULT_NUM_GEN_PATHS, + KWayTransitionCoverTestsIterator.DEFAULT_MAX_PATH_LENGTH, + KWayTransitionCoverTestsIterator.DEFAULT_MAX_NUM_STEPS, + KWayTransitionCoverTestsIterator.DEFAULT_K, + method, + metric)); + Assert.assertTrue(tests.isEmpty()); + } + + @Test(dataProvider = "config") + public void testRandomAutomaton(GenerationMethod method, OptimizationMetric metric) { + final CompactMealy mealy = + RandomAutomata.randomMealy(new Random(42), AUTOMATON_SIZE, ALPHABET, Alphabets.integers(0, 2)); + + final List> tests = IteratorUtil.list(new KWayTransitionCoverTestsIterator<>(mealy, + ALPHABET, + new Random(42), + KWayTransitionCoverTestsIterator.DEFAULT_R_WALK_LEN, + KWayTransitionCoverTestsIterator.DEFAULT_NUM_GEN_PATHS, + KWayTransitionCoverTestsIterator.DEFAULT_MAX_PATH_LENGTH, + KWayTransitionCoverTestsIterator.DEFAULT_MAX_NUM_STEPS, + KWayTransitionCoverTestsIterator.DEFAULT_K, + method, + metric)); + KWayStateCoverTestsIteratorTest.verifyEachStateVisited(mealy, tests); + } + + @Test(dataProvider = "config") + public void testKeylockAutomaton(GenerationMethod method, OptimizationMetric metric) { + final CompactDFA mealy = KWayStateCoverTestsIteratorTest.generateKeylockAutomaton(ALPHABET); + final List> tests = IteratorUtil.list(new KWayTransitionCoverTestsIterator<>(mealy, + ALPHABET, + new Random(42), + KWayTransitionCoverTestsIterator.DEFAULT_R_WALK_LEN, + KWayTransitionCoverTestsIterator.DEFAULT_NUM_GEN_PATHS, + KWayTransitionCoverTestsIterator.DEFAULT_MAX_PATH_LENGTH, + KWayTransitionCoverTestsIterator.DEFAULT_MAX_NUM_STEPS, + KWayTransitionCoverTestsIterator.DEFAULT_K, + method, + metric)); + KWayStateCoverTestsIteratorTest.verifyEachStateVisited(mealy, tests); } } From e5f7d9ce529100d6e904d12ab8a6391ea834a878 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Sat, 9 Aug 2025 20:30:53 +0200 Subject: [PATCH 4/9] finalizing things bugfixes, documentation, performance improvements --- .../KWayStateCoverTestsIterator.java | 10 +- .../KWayTransitionCoverTestsIterator.java | 255 ++++++++++-------- .../KWayTransitionCoverTestsIteratorTest.java | 79 ++++++ 3 files changed, 234 insertions(+), 110 deletions(-) diff --git a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java index bca69ebbc..1c86caab6 100644 --- a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java +++ b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java @@ -35,6 +35,7 @@ import net.automatalib.util.graph.apsp.APSPResult; import net.automatalib.word.Word; import net.automatalib.word.WordBuilder; +import org.checkerframework.checker.nullness.qual.Nullable; /** * A randomized state cover test generator based on the concepts of mutation testing as described in the paper > { private final Iterator> combIter; - private final Set>>> cache; + private final Set>>> cache; private final S initial; private APSPResult> apsp; @@ -193,7 +194,8 @@ protected boolean calculateNext() { while (combIter.hasNext()) { final List comb = combIter.next(); - final Set>> prefixes = new HashSet<>(HashUtil.capacity(comb.size())); + final Set<@Nullable List>> prefixes = + new HashSet<>(HashUtil.capacity(comb.size())); for (S c : comb) { prefixes.add(apsp.getShortestPath(initial, c)); @@ -225,8 +227,8 @@ protected boolean calculateNext() { break; } - for (TransitionEdge pathBetweenState : pathBetweenStates) { - pathBuilder.append(pathBetweenState.getInput()); + for (TransitionEdge t : pathBetweenStates) { + pathBuilder.append(t.getInput()); } } diff --git a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java index 0380bc84d..30f0fc58c 100644 --- a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java +++ b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java @@ -21,7 +21,6 @@ import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; -import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; import java.util.Random; @@ -39,13 +38,14 @@ import net.automatalib.util.graph.apsp.APSPResult; import net.automatalib.word.Word; import net.automatalib.word.WordBuilder; +import org.checkerframework.checker.nullness.qual.Nullable; /** - * A randomized state cover test generator 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 iterates selects test cases based on k-way transitions coverage. It does that by generating random test words + * This iterator selects test cases based on k-way transitions coverage. It does that by generating random test words * and finding the smallest subset with the highest coverage. In other words, this iterator generates test words by * running random paths that cover all pairwise / k-way transitions. * @@ -131,20 +131,20 @@ public KWayTransitionCoverTestsIterator(A automaton, Collection inp * @param random * the random number generator to use * @param randomWalkLen - * the number of steps that are added by 'prefix' generated paths + * the number of steps that are added by {@link GenerationMethod#PREFIX prefix}-generated paths * @param numGeneratePaths - * number of random queries used to find the optimal subset + * number of {@link GenerationMethod#RANDOM randomly}-generated queries used to find the optimal subset * @param maxPathLen - * the maximum step size of a generated path + * the maximum step size of {@link GenerationMethod#RANDOM randomly}-generated paths * @param maxNumberOfSteps - * maximum number of steps that will be executed on the automaton (<=0 = no limit) + * threshold for the number of steps after which no more new test words will be generated (<=0 = 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 + * k value used for K-Way transitions, i.e.,the number of steps between the start and the end of a * transition * @param generationMethod - * defines how the queries are generated 'random' or 'prefix' + * defines how the queries are generated * @param optimizationMetric - * minimize either the number of 'steps' or 'queries' that are executed + * the metric after which test cases are minimized */ public KWayTransitionCoverTestsIterator(A automaton, Collection inputs, @@ -172,7 +172,7 @@ public KWayTransitionCoverTestsIterator(A automaton, if (automaton.size() == 0 || initial == null) { this.iterator = Collections.emptyIterator(); } else { - this.iterator = generationMethod.getIterator(this); + this.iterator = generationMethod.getIterator(this, initial); } } @@ -186,128 +186,156 @@ public Word next() { return iterator.next(); } - private Set> generateRandomPaths(A hypothesis) { - Set> result = new LinkedHashSet<>(HashUtil.capacity(numGeneratePaths)); + private Set> generateRandomPaths(A hypothesis, S initial) { + final Set> result = new HashSet<>(HashUtil.capacity(numGeneratePaths)); for (int i = 0; i < numGeneratePaths; i++) { - int randomLength = random.nextInt(maxPathLen - k + 1) + k; // Ensuring length is at least `k` - Word steps = Word.fromList(RandomUtil.sample(random, alphabet, randomLength)); - Path path = createPath(hypothesis, steps); + final int randomLength = random.nextInt(maxPathLen - k + 1) + k; // Ensuring length is at least `k` + final Word steps = Word.fromList(RandomUtil.sample(random, alphabet, randomLength)); + final Path path = createPath(hypothesis, initial, steps); result.add(path); } return result; } - private Path createPath(A hypothesis, Word steps) { - Set> transitions = new HashSet<>(); - List> transitionsLog = new ArrayList<>(); + private Path createPath(A hypothesis, S initial, Word steps) { + final Set> transitions = new HashSet<>(); - List prevStates = new ArrayList<>(steps.size()); - List endStates = new ArrayList<>(steps.size()); + final List prevStates = new ArrayList<>(steps.size()); + final List endStates = new ArrayList<>(steps.size()); - S iter = hypothesis.getInitialState(); + S iter = initial; + Word reachableSteps = steps; - for (I i : steps) { - prevStates.add(iter); - iter = hypothesis.getSuccessor(iter, i); - endStates.add(iter); + for (int i = 0; i < steps.length(); i++) { + final S succ = hypothesis.getSuccessor(iter, steps.getSymbol(i)); + if (succ == null) { + reachableSteps = steps.subWord(0, i); + } else { + prevStates.add(iter); + endStates.add(succ); + iter = succ; + } } - for (int i = 0; i < steps.size() - k + 1; i++) { - S prevState = prevStates.get(i); - S endState = endStates.get(i + k - 1); - Word chunk = steps.subWord(i, i + k); + for (int i = 0; i < reachableSteps.size() - k + 1; i++) { + final S prevState = prevStates.get(i); + final S endState = endStates.get(i + k - 1); + final Word chunk = steps.subWord(i, i + k); - KWayTransition transition = new KWayTransition<>(prevState, endState, chunk); + final KWayTransition transition = new KWayTransition<>(prevState, endState, chunk); - transitionsLog.add(transition); transitions.add(transition); } - return new Path<>(hypothesis.getInitialState(), - endStates.get(endStates.size() - 1), - steps, - transitions, - transitionsLog); + return new Path<>(steps, transitions); } - private Iterator> generatePrefixSteps(A hypothesis) { - List states = new ArrayList<>(hypothesis.getStates()); + private Iterator> generatePrefixSteps(A hypothesis, S initial) { + final List states = new ArrayList<>(hypothesis.getStates()); Collections.reverse(states); - return new PrefixStepsIterator(states.iterator()); + return new PrefixStepsIterator(states.iterator(), initial); + } + + private @Nullable Path selectOptimalPath(Set> covered, Set> paths) { + final Path max = Collections.max(paths, optimizationMetric.getPathComparator(covered)); + return sizeOfSetDifference(max.kWayTransitions, covered) > 0 ? max : null; } - private Path selectOptimalPath(Set> covered, Collection> paths) { - Path max = Collections.max(paths, optimizationMetric.getPathComparator(covered)); - return max.kWayTransitions.size() == covered.size() ? null : max; + static int sizeOfSetDifference(Set minuend, Set subtrahend) { + /* + * This method is performance-critical so we do a little bit more involved computation. + */ + final Set smaller, bigger; + if (minuend.size() < subtrahend.size()) { + smaller = minuend; + bigger = subtrahend; + } else { + smaller = subtrahend; + bigger = minuend; + } + + int size = minuend.size(); + for (T t : smaller) { + if (bigger.contains(t)) { + size--; + } + } + return size; } + /** + * Method by which the prefixes of test words should be generated. + */ public enum GenerationMethod { + /** + * Generate prefixes randomly. + */ RANDOM { @Override > Iterator> getIterator( - KWayTransitionCoverTestsIterator self) { - return self.new GreedySetCoverIterator(); + KWayTransitionCoverTestsIterator self, + S initial) { + return self.new GreedySetCoverIterator(initial); } }, + /** + * Generate prefixes based on access sequences. + */ PREFIX { @Override > Iterator> getIterator( - KWayTransitionCoverTestsIterator self) { - return self.generatePrefixSteps(self.automaton); + KWayTransitionCoverTestsIterator self, + S initial) { + return self.generatePrefixSteps(self.automaton, initial); } }; abstract > Iterator> getIterator( - KWayTransitionCoverTestsIterator self); + KWayTransitionCoverTestsIterator self, + S initial); } + /** + * The metric by which to optimize path selection. + */ public enum OptimizationMetric { + /** + * Selects the paths maximum coverage per step, thus reducing the number of total steps. + */ STEPS { @Override Comparator> getPathComparator(Set> covered) { - return Comparator.comparingDouble(p -> ((double) computeSizeOfDiff(p.kWayTransitions, covered)) / + return Comparator.comparingDouble(p -> ((double) sizeOfSetDifference(p.kWayTransitions, covered)) / p.steps.size()); } }, + /** + * Selects the paths with maximum coverage, thus reducing number of test words. + */ QUERIES { @Override Comparator> getPathComparator(Set> covered) { - return Comparator.comparingDouble(p -> p.kWayTransitions.size() - covered.size()); + return Comparator.comparingDouble(p -> sizeOfSetDifference(p.kWayTransitions, covered)); } }; abstract Comparator> getPathComparator(Set> covered); - - /** - * Computes the size of the set difference without actually materializing the difference. - * - * @return size of the difference - */ - private static int computeSizeOfDiff(Set> transitions, - Set> covered) { - int size = transitions.size(); - for (KWayTransition t : covered) { - if (transitions.contains(t)) { - size--; - } - } - - return size; - } } private class GreedySetCoverIterator extends AbstractSimplifiedIterator> { + private final S initial; private final Set> paths; - private final int sizeOfUniverse; private final Set> covered; + private final int sizeOfUniverse; private int stepCount; - GreedySetCoverIterator() { - this.paths = generateRandomPaths(automaton); + GreedySetCoverIterator(S initial) { + this.initial = initial; + this.paths = generateRandomPaths(automaton, initial); this.covered = new HashSet<>(); this.stepCount = 0; this.sizeOfUniverse = automaton.getStates().size() * (int) Math.pow(alphabet.size(), k); @@ -315,8 +343,8 @@ private class GreedySetCoverIterator extends AbstractSimplifiedIterator> @Override protected boolean calculateNext() { - if (sizeOfUniverse > covered.size()) { - Path path = selectOptimalPath(covered, paths); + while (sizeOfUniverse > covered.size()) { + final Path path = selectOptimalPath(covered, paths); if (path != null) { covered.addAll(path.kWayTransitions); @@ -327,11 +355,14 @@ protected boolean calculateNext() { } if (paths.isEmpty()) { - Iterator> prefixIterator = generatePrefixSteps(automaton); + final Iterator> prefixIterator = generatePrefixSteps(automaton, initial); while (prefixIterator.hasNext()) { - Word generatePrefixStep = prefixIterator.next(); - paths.add(createPath(automaton, generatePrefixStep)); + final Word generatePrefixStep = prefixIterator.next(); + paths.add(createPath(automaton, initial, generatePrefixStep)); } + } else { + // prevent infinite loops + return false; } if (maxNumberOfSteps != 0 && stepCount > maxNumberOfSteps) { @@ -347,21 +378,27 @@ private class PrefixStepsIterator extends AbstractTwoLevelIterator, W private final APSPResult> apsp; private final S initial; - PrefixStepsIterator(Iterator listIterator) { - super(listIterator); + PrefixStepsIterator(Iterator iterator, S initial) { + super(iterator); this.apsp = Graphs.findAPSP(automaton.transitionGraphView(alphabet)); - this.initial = automaton.getInitialState(); + this.initial = initial; } @Override protected Iterator> l2Iterator(S state) { - Iterable> lists = IterableUtil.allTuples(alphabet, k); + /* + * The original code shuffles all tuples globally. Since we can't do this lazily, we approximate this + * behavior by at least shuffling the input symbols for a randomized tuple order. + */ + final List inputs = new ArrayList<>(alphabet); + Collections.shuffle(inputs, random); + final Iterable> lists = IterableUtil.allTuples(inputs, k); return lists.iterator(); } @Override protected Word combine(S state, List steps) { - List> prefix = apsp.getShortestPath(initial, state); + final List> prefix = apsp.getShortestPath(initial, state); if (prefix == null) { return Word.epsilon(); } @@ -384,71 +421,77 @@ private static final class KWayTransition { private final S endState; private final Word steps; + /** + * Since we need to compute the hash code quite often, cache it since we're immutable. + */ + private final int hashCode; + KWayTransition(S startState, S endState, Word steps) { this.startState = startState; this.endState = endState; this.steps = steps; + + this.hashCode = computeHashCode(startState, endState, steps); + } + + private int computeHashCode(S startState, S endState, Word steps) { + final int prime = 31; + int result = 1; + result = prime * result + Objects.hashCode(startState); + result = prime * result + Objects.hashCode(endState); + result = prime * result + Objects.hashCode(steps); + return result; } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { return false; } - KWayTransition that = (KWayTransition) o; + final KWayTransition that = (KWayTransition) o; return Objects.equals(startState, that.startState) && Objects.equals(endState, that.endState) && Objects.equals(steps, that.steps); } @Override public int hashCode() { - int result = Objects.hashCode(startState); - result = 31 * result + Objects.hashCode(endState); - result = 31 * result + Objects.hashCode(steps); - return result; + return hashCode; } } private static final class Path { - private final S startState; - private final S endState; private final Word steps; private final Set> kWayTransitions; - private final List> transitionsLog; - Path(S startState, - S endState, - Word steps, - Set> kWayTransitions, - List> transitionsLog) { - this.startState = startState; - this.endState = endState; + Path(Word steps, Set> kWayTransitions) { this.steps = steps; this.kWayTransitions = kWayTransitions; - this.transitionsLog = transitionsLog; } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { + if (o == this) { + return true; + } + if (o == null || getClass() != o.getClass()) { return false; } - Path path = (Path) o; - return Objects.equals(startState, path.startState) && Objects.equals(endState, path.endState) && - Objects.equals(steps, path.steps) && Objects.equals(kWayTransitions, path.kWayTransitions) && - Objects.equals(transitionsLog, path.transitionsLog); + final Path path = (Path) o; + return Objects.equals(steps, path.steps) && Objects.equals(kWayTransitions, path.kWayTransitions); } @Override public int hashCode() { - int result = Objects.hashCode(startState); - result = 31 * result + Objects.hashCode(endState); - result = 31 * result + Objects.hashCode(steps); + int result = Objects.hashCode(steps); result = 31 * result + Objects.hashCode(kWayTransitions); - result = 31 * result + Objects.hashCode(transitionsLog); return result; } } diff --git a/util/src/test/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIteratorTest.java b/util/src/test/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIteratorTest.java index d4c7ee535..fa6d98297 100644 --- a/util/src/test/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIteratorTest.java +++ b/util/src/test/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIteratorTest.java @@ -17,6 +17,7 @@ import java.util.List; import java.util.Random; +import java.util.Set; import net.automatalib.alphabet.Alphabet; import net.automatalib.alphabet.impl.Alphabets; @@ -51,6 +52,30 @@ public static Object[][] getConfig() { return result; } + @Test + public void testSizeOfSetDifference() { + final Set s1 = Set.of('A', 'B', 'C'); + final Set s2 = Set.of('A', 'B'); + final Set s3 = Set.of('A'); + final Set s4 = Set.of('X', 'Y', 'Z'); + final Set s5 = Set.of('A', 'X'); + + Assert.assertEquals(KWayTransitionCoverTestsIterator.sizeOfSetDifference(s1, s2), 1); + Assert.assertEquals(KWayTransitionCoverTestsIterator.sizeOfSetDifference(s2, s1), 0); + + Assert.assertEquals(KWayTransitionCoverTestsIterator.sizeOfSetDifference(s1, s3), 2); + Assert.assertEquals(KWayTransitionCoverTestsIterator.sizeOfSetDifference(s3, s1), 0); + + Assert.assertEquals(KWayTransitionCoverTestsIterator.sizeOfSetDifference(s1, s4), 3); + Assert.assertEquals(KWayTransitionCoverTestsIterator.sizeOfSetDifference(s4, s1), 3); + + Assert.assertEquals(KWayTransitionCoverTestsIterator.sizeOfSetDifference(s1, s5), 2); + Assert.assertEquals(KWayTransitionCoverTestsIterator.sizeOfSetDifference(s5, s1), 1); + + Assert.assertEquals(KWayTransitionCoverTestsIterator.sizeOfSetDifference(s4, s5), 2); + Assert.assertEquals(KWayTransitionCoverTestsIterator.sizeOfSetDifference(s5, s4), 1); + } + @Test public void testEmptyAutomaton() { final CompactDFA dfa = new CompactDFA<>(ALPHABET); @@ -133,4 +158,58 @@ public void testKeylockAutomaton(GenerationMethod method, OptimizationMetric met metric)); KWayStateCoverTestsIteratorTest.verifyEachStateVisited(mealy, tests); } + + @Test + public void testLimits() { + final Random random = new Random(42); + final Alphabet alphabet = Alphabets.integers(0, 6); + final CompactDFA dfa = RandomAutomata.randomDFA(random, 10, alphabet, false); + final int randomWalkLen = 50; + + final List> rWalkTests = IteratorUtil.list(new KWayTransitionCoverTestsIterator<>(dfa, + alphabet, + random, + randomWalkLen, + KWayTransitionCoverTestsIterator.DEFAULT_NUM_GEN_PATHS, + KWayTransitionCoverTestsIterator.DEFAULT_MAX_PATH_LENGTH, + KWayTransitionCoverTestsIterator.DEFAULT_MAX_NUM_STEPS, + KWayTransitionCoverTestsIterator.DEFAULT_K, + GenerationMethod.PREFIX, + OptimizationMetric.QUERIES)); + + for (Word t : rWalkTests) { + Assert.assertTrue(t.size() > randomWalkLen, t.toString()); + } + + final int maxPathLength = 10; + final List> maxPathTests = IteratorUtil.list(new KWayTransitionCoverTestsIterator<>(dfa, + alphabet, + random, + KWayTransitionCoverTestsIterator.DEFAULT_R_WALK_LEN, + KWayTransitionCoverTestsIterator.DEFAULT_NUM_GEN_PATHS, + maxPathLength, + KWayTransitionCoverTestsIterator.DEFAULT_MAX_NUM_STEPS, + KWayTransitionCoverTestsIterator.DEFAULT_K, + GenerationMethod.RANDOM, + OptimizationMetric.QUERIES)); + + for (Word t : maxPathTests) { + Assert.assertTrue(t.size() <= maxPathLength, t.toString()); + } + + final int maxNumSteps = 20; + final List> maxNumStepsTests = IteratorUtil.list(new KWayTransitionCoverTestsIterator<>(dfa, + alphabet, + random, + KWayTransitionCoverTestsIterator.DEFAULT_R_WALK_LEN, + KWayTransitionCoverTestsIterator.DEFAULT_NUM_GEN_PATHS, + KWayTransitionCoverTestsIterator.DEFAULT_MAX_PATH_LENGTH, + maxNumSteps, + KWayTransitionCoverTestsIterator.DEFAULT_K, + GenerationMethod.RANDOM, + OptimizationMetric.STEPS)); + + Assert.assertTrue(maxNumStepsTests.stream().mapToInt(Word::size).sum() >= maxNumSteps, + maxNumStepsTests.toString()); + } } From 4e4b3c89069c135c9cccb56fc2643da6d109a944 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Sun, 10 Aug 2025 02:51:02 +0200 Subject: [PATCH 5/9] reorder attributes --- .../KWayStateCoverTestsIterator.java | 6 +- .../KWayTransitionCoverTestsIterator.java | 145 +++++++++--------- .../KWayTransitionCoverTestsIteratorTest.java | 28 ++-- 3 files changed, 90 insertions(+), 89 deletions(-) diff --git a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java index 1c86caab6..6b11135d5 100644 --- a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java +++ b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java @@ -63,8 +63,8 @@ public class KWayStateCoverTestsIterator alphabet; private final Random random; - private final int k; private final int randomWalkLen; + private final int k; private final CombinationMethod method; private final Iterator> iterator; @@ -125,10 +125,10 @@ public KWayStateCoverTestsIterator(A automaton, CombinationMethod method) { this.automaton = automaton; this.alphabet = CollectionUtil.randomAccessList(inputs); - this.k = Math.min(k, automaton.size()); + this.random = random; this.randomWalkLen = randomWalkLen; + this.k = Math.min(k, automaton.size()); this.method = method; - this.random = random; final S initial = automaton.getInitialState(); diff --git a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java index 30f0fc58c..9960ae697 100644 --- a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java +++ b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java @@ -70,12 +70,12 @@ public class KWayTransitionCoverTestsIterator alphabet; private final Random random; - private final int k; + 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 int randomWalkLen; private final Iterator> iterator; @@ -96,7 +96,7 @@ public KWayTransitionCoverTestsIterator(A automaton, Collection inp /** * Convenience constructor. Uses {@code randomWalkLen=10}, {@code numGeneratePaths = 1_000}, * {@code maxPathLen = 50}, {@code maxNumberOfSteps = 0}, {@code k = 2}, - * {@code generationMethod = GenerationMethod.RANDOM}, and {@code optimizationMetric = OptimizationMetric.STEPS}. + * {@code optimizationMetric = OptimizationMetric.STEPS}, and {@code generationMethod = GenerationMethod.RANDOM}. * * @param automaton * the automaton for which to generate test cases @@ -106,7 +106,7 @@ public KWayTransitionCoverTestsIterator(A automaton, Collection inp * the random number generator to use * * @see #KWayTransitionCoverTestsIterator(UniversalDeterministicAutomaton, Collection, Random, int, int, int, int, - * int, GenerationMethod, OptimizationMetric) + * int, OptimizationMetric, GenerationMethod) */ public KWayTransitionCoverTestsIterator(A automaton, Collection inputs, Random random) { this(automaton, @@ -117,8 +117,8 @@ public KWayTransitionCoverTestsIterator(A automaton, Collection inp DEFAULT_MAX_PATH_LENGTH, DEFAULT_MAX_NUM_STEPS, DEFAULT_K, - GenerationMethod.RANDOM, - OptimizationMetric.STEPS); + OptimizationMetric.STEPS, + GenerationMethod.RANDOM); } /** @@ -133,7 +133,7 @@ public KWayTransitionCoverTestsIterator(A automaton, Collection inp * @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 + * number of {@link GenerationMethod#RANDOM randomly}-generated tests used to find the optimal subset * @param maxPathLen * the maximum step size of {@link GenerationMethod#RANDOM randomly}-generated paths * @param maxNumberOfSteps @@ -141,10 +141,10 @@ public KWayTransitionCoverTestsIterator(A automaton, Collection inp * @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 generationMethod - * defines how the queries are generated * @param optimizationMetric * the metric after which test cases are minimized + * @param generationMethod + * defines how the tests are generated */ public KWayTransitionCoverTestsIterator(A automaton, Collection inputs, @@ -154,18 +154,18 @@ public KWayTransitionCoverTestsIterator(A automaton, int maxPathLen, int maxNumberOfSteps, int k, - GenerationMethod generationMethod, - OptimizationMetric optimizationMetric) { + OptimizationMetric optimizationMetric, + GenerationMethod generationMethod) { this.automaton = automaton; this.alphabet = CollectionUtil.randomAccessList(inputs); this.random = random; - this.k = Math.min(k, automaton.size()); this.numGeneratePaths = numGeneratePaths; this.maxPathLen = maxPathLen; + this.randomWalkLen = randomWalkLen; this.maxNumberOfSteps = maxNumberOfSteps; + this.k = Math.min(k, automaton.size()); this.optimizationMetric = optimizationMetric; - this.randomWalkLen = randomWalkLen; final S initial = automaton.getInitialState(); @@ -174,6 +174,7 @@ public KWayTransitionCoverTestsIterator(A automaton, } else { this.iterator = generationMethod.getIterator(this, initial); } + } @Override @@ -265,65 +266,6 @@ static int sizeOfSetDifference(Set minuend, Set subtrahend) { return size; } - /** - * Method by which the prefixes of test words should be generated. - */ - public enum GenerationMethod { - /** - * Generate prefixes randomly. - */ - RANDOM { - @Override - > Iterator> getIterator( - KWayTransitionCoverTestsIterator self, - S initial) { - return self.new GreedySetCoverIterator(initial); - } - }, - /** - * Generate prefixes based on access sequences. - */ - PREFIX { - @Override - > Iterator> getIterator( - KWayTransitionCoverTestsIterator self, - S initial) { - return self.generatePrefixSteps(self.automaton, initial); - } - }; - - abstract > Iterator> getIterator( - KWayTransitionCoverTestsIterator self, - S initial); - } - - /** - * The metric by which to optimize path selection. - */ - public enum OptimizationMetric { - /** - * Selects the paths maximum coverage per step, thus reducing the number of total steps. - */ - STEPS { - @Override - Comparator> getPathComparator(Set> covered) { - return Comparator.comparingDouble(p -> ((double) sizeOfSetDifference(p.kWayTransitions, covered)) / - p.steps.size()); - } - }, - /** - * Selects the paths with maximum coverage, thus reducing number of test words. - */ - QUERIES { - @Override - Comparator> getPathComparator(Set> covered) { - return Comparator.comparingDouble(p -> sizeOfSetDifference(p.kWayTransitions, covered)); - } - }; - - abstract Comparator> getPathComparator(Set> covered); - } - private class GreedySetCoverIterator extends AbstractSimplifiedIterator> { private final S initial; @@ -495,4 +437,63 @@ public int hashCode() { return result; } } + + /** + * Method by which the prefixes of test words should be generated. + */ + public enum GenerationMethod { + /** + * Generate prefixes randomly. + */ + RANDOM { + @Override + > Iterator> getIterator( + KWayTransitionCoverTestsIterator self, + S initial) { + return self.new GreedySetCoverIterator(initial); + } + }, + /** + * Generate prefixes based on access sequences. + */ + PREFIX { + @Override + > Iterator> getIterator( + KWayTransitionCoverTestsIterator self, + S initial) { + return self.generatePrefixSteps(self.automaton, initial); + } + }; + + abstract > Iterator> getIterator( + KWayTransitionCoverTestsIterator self, + S initial); + } + + /** + * The metric by which to optimize path selection. + */ + public enum OptimizationMetric { + /** + * Selects the paths maximum coverage per step, thus reducing the number of total steps. + */ + STEPS { + @Override + Comparator> getPathComparator(Set> covered) { + return Comparator.comparingDouble(p -> ((double) sizeOfSetDifference(p.kWayTransitions, covered)) / + p.steps.size()); + } + }, + /** + * Selects the paths with maximum coverage, thus reducing number of test words. + */ + QUERIES { + @Override + Comparator> getPathComparator(Set> covered) { + return Comparator.comparingDouble(p -> sizeOfSetDifference(p.kWayTransitions, covered)); + } + }; + + abstract Comparator> getPathComparator(Set> covered); + } } diff --git a/util/src/test/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIteratorTest.java b/util/src/test/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIteratorTest.java index fa6d98297..b6c88eb87 100644 --- a/util/src/test/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIteratorTest.java +++ b/util/src/test/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIteratorTest.java @@ -101,8 +101,8 @@ public void testSingleStateAutomaton(GenerationMethod method, OptimizationMetric KWayTransitionCoverTestsIterator.DEFAULT_MAX_PATH_LENGTH, KWayTransitionCoverTestsIterator.DEFAULT_MAX_NUM_STEPS, KWayTransitionCoverTestsIterator.DEFAULT_K, - method, - metric)); + metric, + method)); KWayStateCoverTestsIteratorTest.verifyEachStateVisited(dfa, tests); } @@ -119,8 +119,8 @@ public void testNoInitialStateAutomaton(GenerationMethod method, OptimizationMet KWayTransitionCoverTestsIterator.DEFAULT_MAX_PATH_LENGTH, KWayTransitionCoverTestsIterator.DEFAULT_MAX_NUM_STEPS, KWayTransitionCoverTestsIterator.DEFAULT_K, - method, - metric)); + metric, + method)); Assert.assertTrue(tests.isEmpty()); } @@ -137,8 +137,8 @@ public void testRandomAutomaton(GenerationMethod method, OptimizationMetric metr KWayTransitionCoverTestsIterator.DEFAULT_MAX_PATH_LENGTH, KWayTransitionCoverTestsIterator.DEFAULT_MAX_NUM_STEPS, KWayTransitionCoverTestsIterator.DEFAULT_K, - method, - metric)); + metric, + method)); KWayStateCoverTestsIteratorTest.verifyEachStateVisited(mealy, tests); } @@ -154,8 +154,8 @@ public void testKeylockAutomaton(GenerationMethod method, OptimizationMetric met KWayTransitionCoverTestsIterator.DEFAULT_MAX_PATH_LENGTH, KWayTransitionCoverTestsIterator.DEFAULT_MAX_NUM_STEPS, KWayTransitionCoverTestsIterator.DEFAULT_K, - method, - metric)); + metric, + method)); KWayStateCoverTestsIteratorTest.verifyEachStateVisited(mealy, tests); } @@ -174,8 +174,8 @@ public void testLimits() { KWayTransitionCoverTestsIterator.DEFAULT_MAX_PATH_LENGTH, KWayTransitionCoverTestsIterator.DEFAULT_MAX_NUM_STEPS, KWayTransitionCoverTestsIterator.DEFAULT_K, - GenerationMethod.PREFIX, - OptimizationMetric.QUERIES)); + OptimizationMetric.QUERIES, + GenerationMethod.PREFIX)); for (Word t : rWalkTests) { Assert.assertTrue(t.size() > randomWalkLen, t.toString()); @@ -190,8 +190,8 @@ public void testLimits() { maxPathLength, KWayTransitionCoverTestsIterator.DEFAULT_MAX_NUM_STEPS, KWayTransitionCoverTestsIterator.DEFAULT_K, - GenerationMethod.RANDOM, - OptimizationMetric.QUERIES)); + OptimizationMetric.QUERIES, + GenerationMethod.RANDOM)); for (Word t : maxPathTests) { Assert.assertTrue(t.size() <= maxPathLength, t.toString()); @@ -206,8 +206,8 @@ public void testLimits() { KWayTransitionCoverTestsIterator.DEFAULT_MAX_PATH_LENGTH, maxNumSteps, KWayTransitionCoverTestsIterator.DEFAULT_K, - GenerationMethod.RANDOM, - OptimizationMetric.STEPS)); + OptimizationMetric.STEPS, + GenerationMethod.RANDOM)); Assert.assertTrue(maxNumStepsTests.stream().mapToInt(Word::size).sum() >= maxNumSteps, maxNumStepsTests.toString()); From fa0190f40ef1cc9d45ba822b2e0c39d298ed00c1 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Sun, 10 Aug 2025 14:30:26 +0200 Subject: [PATCH 6/9] fix logic flaw and support partial automata --- .../KWayStateCoverTestsIterator.java | 23 ++++--- .../KWayTransitionCoverTestsIterator.java | 61 ++++++++----------- .../KWayStateCoverTestsIteratorTest.java | 33 +++++++++- .../KWayTransitionCoverTestsIteratorTest.java | 45 ++++++++++++-- 4 files changed, 110 insertions(+), 52 deletions(-) diff --git a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java index 6b11135d5..dfd148ce9 100644 --- a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java +++ b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java @@ -35,7 +35,6 @@ import net.automatalib.util.graph.apsp.APSPResult; import net.automatalib.word.Word; import net.automatalib.word.WordBuilder; -import org.checkerframework.checker.nullness.qual.Nullable; /** * A randomized state cover test generator based on the concepts of mutation testing as described in the paper > { private final Iterator> combIter; - private final Set>>> cache; + private final Set>>> cache; private final S initial; private APSPResult> apsp; @@ -194,22 +193,26 @@ protected boolean calculateNext() { while (combIter.hasNext()) { final List comb = combIter.next(); - final Set<@Nullable List>> prefixes = - new HashSet<>(HashUtil.capacity(comb.size())); + final Set>> prefixes = new HashSet<>(HashUtil.capacity(comb.size())); + + List> path = null; for (S c : comb) { - prefixes.add(apsp.getShortestPath(initial, c)); + List> sp = apsp.getShortestPath(initial, c); + if (sp != null) { + prefixes.add(sp); + if (path == null) { + path = sp; + } + } } - if (!cache.add(prefixes)) { + if (path == null || !cache.add(prefixes)) { continue; } - final List> firstPath = apsp.getShortestPath(initial, comb.get(0)); - assert firstPath != null; - final WordBuilder pathBuilder = new WordBuilder<>(); - for (TransitionEdge e : firstPath) { + for (TransitionEdge e : path) { pathBuilder.append(e.getInput()); } diff --git a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java index 9960ae697..83506d190 100644 --- a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java +++ b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java @@ -203,26 +203,20 @@ private Set> generateRandomPaths(A hypothesis, S initial) { private Path createPath(A hypothesis, S initial, Word steps) { final Set> transitions = new HashSet<>(); - final List prevStates = new ArrayList<>(steps.size()); - final List endStates = new ArrayList<>(steps.size()); + final List<@Nullable S> prevStates = new ArrayList<>(steps.size()); + final List<@Nullable S> endStates = new ArrayList<>(steps.size()); S iter = initial; - Word reachableSteps = steps; - - for (int i = 0; i < steps.length(); i++) { - final S succ = hypothesis.getSuccessor(iter, steps.getSymbol(i)); - if (succ == null) { - reachableSteps = steps.subWord(0, i); - } else { - prevStates.add(iter); - endStates.add(succ); - iter = succ; - } + + for (I i : steps) { + prevStates.add(iter); + iter = iter == null ? null : hypothesis.getSuccessor(iter, i); + endStates.add(iter); } - for (int i = 0; i < reachableSteps.size() - k + 1; i++) { - final S prevState = prevStates.get(i); - final S endState = endStates.get(i + k - 1); + for (int i = 0; i < steps.size() - k + 1; i++) { + final @Nullable S prevState = prevStates.get(i); + final @Nullable S endState = endStates.get(i + k - 1); final Word chunk = steps.subWord(i, i + k); final KWayTransition transition = new KWayTransition<>(prevState, endState, chunk); @@ -285,7 +279,7 @@ private class GreedySetCoverIterator extends AbstractSimplifiedIterator> @Override protected boolean calculateNext() { - while (sizeOfUniverse > covered.size()) { + while (sizeOfUniverse > covered.size() && (maxNumberOfSteps == 0 || stepCount <= maxNumberOfSteps)) { final Path path = selectOptimalPath(covered, paths); if (path != null) { @@ -293,26 +287,25 @@ protected boolean calculateNext() { paths.remove(path); stepCount += path.steps.size(); super.nextValue = path.steps; - return true; - } - if (paths.isEmpty()) { - final Iterator> prefixIterator = generatePrefixSteps(automaton, initial); - while (prefixIterator.hasNext()) { - final Word generatePrefixStep = prefixIterator.next(); - paths.add(createPath(automaton, initial, generatePrefixStep)); + if (paths.isEmpty()){ + computeNewPaths(); } + return true; } else { - // prevent infinite loops - return false; - } - - if (maxNumberOfSteps != 0 && stepCount > maxNumberOfSteps) { - return false; + computeNewPaths(); } } return false; } + + private void computeNewPaths() { + final Iterator> prefixIterator = generatePrefixSteps(automaton, initial); + while (prefixIterator.hasNext()) { + final Word generatePrefixStep = prefixIterator.next(); + paths.add(createPath(automaton, initial, generatePrefixStep)); + } + } } private class PrefixStepsIterator extends AbstractTwoLevelIterator, Word> { @@ -359,8 +352,8 @@ protected Word combine(S state, List steps) { private static final class KWayTransition { - private final S startState; - private final S endState; + private final @Nullable S startState; + private final @Nullable S endState; private final Word steps; /** @@ -368,7 +361,7 @@ private static final class KWayTransition { */ private final int hashCode; - KWayTransition(S startState, S endState, Word steps) { + KWayTransition(@Nullable S startState, @Nullable S endState, Word steps) { this.startState = startState; this.endState = endState; this.steps = steps; @@ -376,7 +369,7 @@ private static final class KWayTransition { this.hashCode = computeHashCode(startState, endState, steps); } - private int computeHashCode(S startState, S endState, Word steps) { + private int computeHashCode(@Nullable S startState, @Nullable S endState, Word steps) { final int prime = 31; int result = 1; result = prime * result + Objects.hashCode(startState); diff --git a/util/src/test/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIteratorTest.java b/util/src/test/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIteratorTest.java index cf48f095e..1fcd94561 100644 --- a/util/src/test/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIteratorTest.java +++ b/util/src/test/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIteratorTest.java @@ -27,6 +27,7 @@ import net.automatalib.automaton.transducer.impl.CompactMealy; import net.automatalib.common.util.HashUtil; import net.automatalib.common.util.collection.IteratorUtil; +import net.automatalib.util.automaton.builder.AutomatonBuilders; import net.automatalib.util.automaton.conformance.KWayStateCoverTestsIterator.CombinationMethod; import net.automatalib.util.automaton.random.RandomAutomata; import net.automatalib.word.Word; @@ -91,6 +92,26 @@ public void testNoInitialStateAutomaton(CombinationMethod method) { Assert.assertTrue(tests.isEmpty()); } + @Test(dataProvider = "methods") + public void testPartialAutomaton(CombinationMethod method) { + // @formatter:off + final CompactDFA dfa = AutomatonBuilders.newDFA(ALPHABET) + .from("s0").on('a').to("s1") + .from("s1").on('b').to("s2") + .withInitial("s0") + .withAccepting("s2", "s3") + .create(); + // @formatter:on + + final List> tests = IteratorUtil.list(new KWayStateCoverTestsIterator<>(dfa, + ALPHABET, + new Random(42), + KWayStateCoverTestsIterator.DEFAULT_R_WALK_LEN, + KWayStateCoverTestsIterator.DEFAULT_K, + method)); + verifyEachStateVisited(dfa, tests, Set.of(0, 1, 2)); + } + @Test(dataProvider = "methods") public void testRandomAutomaton(CombinationMethod method) { final CompactMealy mealy = @@ -122,6 +143,12 @@ public void testKeylockAutomaton(CombinationMethod method) { static void verifyEachStateVisited(UniversalDeterministicAutomaton automaton, List> tests) { + verifyEachStateVisited(automaton, tests, new HashSet<>(automaton.getStates())); + } + + static void verifyEachStateVisited(UniversalDeterministicAutomaton automaton, + List> tests, + Set expected) { final Set visited = new HashSet<>(HashUtil.capacity(automaton.size())); final S init = automaton.getInitialState(); @@ -133,13 +160,15 @@ static void verifyEachStateVisited(UniversalDeterministicAutomaton(automaton.getStates())); + Assert.assertEquals(visited, expected); } static CompactDFA generateKeylockAutomaton(Alphabet alphabet) { diff --git a/util/src/test/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIteratorTest.java b/util/src/test/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIteratorTest.java index b6c88eb87..0e6e76a59 100644 --- a/util/src/test/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIteratorTest.java +++ b/util/src/test/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIteratorTest.java @@ -24,6 +24,7 @@ import net.automatalib.automaton.fsa.impl.CompactDFA; import net.automatalib.automaton.transducer.impl.CompactMealy; import net.automatalib.common.util.collection.IteratorUtil; +import net.automatalib.util.automaton.builder.AutomatonBuilders; import net.automatalib.util.automaton.conformance.KWayTransitionCoverTestsIterator.GenerationMethod; import net.automatalib.util.automaton.conformance.KWayTransitionCoverTestsIterator.OptimizationMetric; import net.automatalib.util.automaton.random.RandomAutomata; @@ -124,6 +125,30 @@ public void testNoInitialStateAutomaton(GenerationMethod method, OptimizationMet Assert.assertTrue(tests.isEmpty()); } + @Test(dataProvider = "config") + public void testPartialAutomaton(GenerationMethod method, OptimizationMetric metric) { + // @formatter:off + final CompactDFA dfa = AutomatonBuilders.newDFA(ALPHABET) + .from("s0").on('a').to("s1") + .from("s1").on('b').to("s2") + .withInitial("s0") + .withAccepting("s2", "s3") + .create(); + // @formatter:on + + final List> tests = IteratorUtil.list(new KWayTransitionCoverTestsIterator<>(dfa, + ALPHABET, + new Random(42), + KWayTransitionCoverTestsIterator.DEFAULT_R_WALK_LEN, + KWayTransitionCoverTestsIterator.DEFAULT_NUM_GEN_PATHS, + KWayTransitionCoverTestsIterator.DEFAULT_MAX_PATH_LENGTH, + KWayTransitionCoverTestsIterator.DEFAULT_MAX_NUM_STEPS, + KWayTransitionCoverTestsIterator.DEFAULT_K, + metric, + method)); + KWayStateCoverTestsIteratorTest.verifyEachStateVisited(dfa, tests, Set.of(0, 1, 2)); + } + @Test(dataProvider = "config") public void testRandomAutomaton(GenerationMethod method, OptimizationMetric metric) { final CompactMealy mealy = @@ -185,7 +210,8 @@ public void testLimits() { final List> maxPathTests = IteratorUtil.list(new KWayTransitionCoverTestsIterator<>(dfa, alphabet, random, - KWayTransitionCoverTestsIterator.DEFAULT_R_WALK_LEN, + maxPathLength * + maxPathLength, KWayTransitionCoverTestsIterator.DEFAULT_NUM_GEN_PATHS, maxPathLength, KWayTransitionCoverTestsIterator.DEFAULT_MAX_NUM_STEPS, @@ -194,22 +220,29 @@ public void testLimits() { GenerationMethod.RANDOM)); for (Word t : maxPathTests) { - Assert.assertTrue(t.size() <= maxPathLength, t.toString()); + final int length = t.size(); + /* + * In case the random generation can no longer find any meaningful test sequences it re-generates candidates + * via the prefix method which uses the randomWalkLength. We make this value reasonably large to check that + * test sequences were only generated from these two possibilities. + */ + Assert.assertTrue(length <= maxPathLength || length > maxPathLength * maxPathLength, t.toString()); } - final int maxNumSteps = 20; + final int maxNumSteps = 10; final List> maxNumStepsTests = IteratorUtil.list(new KWayTransitionCoverTestsIterator<>(dfa, alphabet, random, KWayTransitionCoverTestsIterator.DEFAULT_R_WALK_LEN, KWayTransitionCoverTestsIterator.DEFAULT_NUM_GEN_PATHS, - KWayTransitionCoverTestsIterator.DEFAULT_MAX_PATH_LENGTH, + maxPathLength, maxNumSteps, KWayTransitionCoverTestsIterator.DEFAULT_K, OptimizationMetric.STEPS, GenerationMethod.RANDOM)); - Assert.assertTrue(maxNumStepsTests.stream().mapToInt(Word::size).sum() >= maxNumSteps, - maxNumStepsTests.toString()); + final int numSteps = maxNumStepsTests.stream().mapToInt(Word::size).sum(); + Assert.assertTrue(numSteps >= maxNumSteps, maxNumStepsTests.toString()); + Assert.assertTrue(numSteps <= maxNumSteps + maxPathLength, maxNumStepsTests.toString()); } } From e2a27eb4997a4cdd27f83351fc24d593583dfcfa Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Thu, 13 Nov 2025 14:31:39 +0100 Subject: [PATCH 7/9] suggest to handle corner-cases externally --- .../collection/CartesianProductIterator.java | 4 +- .../util/collection/IteratorUtilTest.java | 4 +- .../KWayStateCoverTestsIterator.java | 172 ++++++------------ .../KWayTransitionCoverTestsIterator.java | 6 + .../KWayStateCoverTestsIteratorTest.java | 6 +- 5 files changed, 72 insertions(+), 120 deletions(-) diff --git a/commons/util/src/main/java/net/automatalib/common/util/collection/CartesianProductIterator.java b/commons/util/src/main/java/net/automatalib/common/util/collection/CartesianProductIterator.java index 07053506a..efb45b94f 100644 --- a/commons/util/src/main/java/net/automatalib/common/util/collection/CartesianProductIterator.java +++ b/commons/util/src/main/java/net/automatalib/common/util/collection/CartesianProductIterator.java @@ -35,7 +35,7 @@ final class CartesianProductIterator implements Iterator> { private final Iterable[] iterables; private final Iterator[] iterators; private final List current; - private boolean first = true; + private boolean first; private boolean empty; @SuppressWarnings("unchecked") @@ -44,6 +44,8 @@ final class CartesianProductIterator implements Iterator> { this.iterables = iterables; this.iterators = new Iterator[iterables.length]; this.current = new ArrayList<>(iterables.length); + this.first = true; + for (int i = 0; i < iterators.length; i++) { Iterator it = iterables[i].iterator(); if (!it.hasNext()) { diff --git a/commons/util/src/test/java/net/automatalib/common/util/collection/IteratorUtilTest.java b/commons/util/src/test/java/net/automatalib/common/util/collection/IteratorUtilTest.java index 9b579c8af..02e426f60 100644 --- a/commons/util/src/test/java/net/automatalib/common/util/collection/IteratorUtilTest.java +++ b/commons/util/src/test/java/net/automatalib/common/util/collection/IteratorUtilTest.java @@ -27,7 +27,7 @@ public class IteratorUtilTest { @Test public void testBatch() { - final Iterator iterator = Stream.iterate(0, i -> i+1).limit(50).iterator(); + final Iterator iterator = Stream.iterate(0, i -> i + 1).limit(50).iterator(); final Iterator> batchIterator = IteratorUtil.batch(iterator, 20); Assert.assertTrue(batchIterator.hasNext()); @@ -39,7 +39,7 @@ public void testBatch() { Assert.assertFalse(batchIterator.hasNext()); Assert.assertThrows(NoSuchElementException.class, batchIterator::next); - final Iterator iterator2 = Stream.iterate(0, i -> i+1).limit(60).iterator(); + final Iterator iterator2 = Stream.iterate(0, i -> i + 1).limit(60).iterator(); final Iterator> batchIterator2 = IteratorUtil.batch(iterator2, 20); Assert.assertTrue(batchIterator2.hasNext()); diff --git a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java index dfd148ce9..f90bc2917 100644 --- a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java +++ b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java @@ -35,6 +35,7 @@ import net.automatalib.util.graph.apsp.APSPResult; import net.automatalib.word.Word; import net.automatalib.word.WordBuilder; +import org.checkerframework.checker.nullness.qual.Nullable; /** * A randomized state cover test generator based on the concepts of mutation testing as described in the paper * 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 IteratorUtil#concat(Iterator[]) combine} this generator with other + * generators or limit the number of generated test cases. * * @param * automaton state type @@ -54,19 +60,19 @@ * automaton type */ public class KWayStateCoverTestsIterator> - implements Iterator> { + extends AbstractSimplifiedIterator> { public static final int DEFAULT_R_WALK_LEN = 20; public static final int DEFAULT_K = 2; - private final A automaton; private final List alphabet; private final Random random; private final int randomWalkLen; - private final int k; - private final CombinationMethod method; - private final Iterator> iterator; + private final Iterator> combIter; + private final Set>>> cache; + private final @Nullable S initial; + private final APSPResult> apsp; /** * Convenience constructor which uses a fresh {@code random} object. @@ -122,140 +128,80 @@ public KWayStateCoverTestsIterator(A automaton, int randomWalkLen, int k, CombinationMethod method) { - this.automaton = automaton; this.alphabet = CollectionUtil.randomAccessList(inputs); this.random = random; this.randomWalkLen = randomWalkLen; - this.k = Math.min(k, automaton.size()); - this.method = method; - final S initial = automaton.getInitialState(); + this.cache = new HashSet<>(); + this.apsp = Graphs.findAPSP(automaton.transitionGraphView(alphabet)); + this.initial = automaton.getInitialState(); - if (automaton.size() == 0 || initial == null) { - this.iterator = Collections.emptyIterator(); + if (this.initial == null) { + this.combIter = Collections.emptyIterator(); } else { - final FirstPhaseIterator firstIterator = new FirstPhaseIterator(); - final SecondPhaseIterator secondPhaseIterator = new SecondPhaseIterator(initial); - this.iterator = IteratorUtil.concat(firstIterator, secondPhaseIterator); + final List states = new ArrayList<>(automaton.getStates()); + Collections.shuffle(states, random); + this.combIter = method.getCombinations(states, Math.min(k, automaton.size())); } - } - @Override - public boolean hasNext() { - return iterator.hasNext(); } @Override - public Word next() { - return iterator.next(); - } - - /** - * Performs random walks if the automaton only has a single state. - */ - private final class FirstPhaseIterator extends AbstractSimplifiedIterator> { - - private int idx; - - @Override - protected boolean calculateNext() { - if (automaton.size() == 1 && idx++ < randomWalkLen) { - super.nextValue = Word.fromList(RandomUtil.sample(random, alphabet, randomWalkLen)); - return true; - } - return false; - } - } - - /** - * Performs the actual k-way coverage for automata with more than a single state. - */ - private final class SecondPhaseIterator extends AbstractSimplifiedIterator> { - - private final Iterator> combIter; - private final Set>>> cache; - private final S initial; - - private APSPResult> apsp; - - SecondPhaseIterator(S initial) { - this.initial = initial; - List states = new ArrayList<>(automaton.getStates()); - Collections.shuffle(states, random); - this.combIter = method.getCombinations(states, k); - this.cache = new HashSet<>(); - } - - @Override - protected boolean calculateNext() { + protected boolean calculateNext() { - final APSPResult> apsp = getAPSP(); + while (combIter.hasNext()) { + final List comb = combIter.next(); + final Set>> prefixes = new HashSet<>(HashUtil.capacity(comb.size())); - while (combIter.hasNext()) { - final List comb = combIter.next(); - final Set>> prefixes = new HashSet<>(HashUtil.capacity(comb.size())); + List> path = null; + assert initial != null; - List> path = null; - - for (S c : comb) { - List> sp = apsp.getShortestPath(initial, c); - if (sp != null) { - prefixes.add(sp); - if (path == null) { - path = sp; - } + for (S c : comb) { + List> sp = apsp.getShortestPath(initial, c); + if (sp != null) { + prefixes.add(sp); + if (path == null) { + path = sp; } } + } - if (path == null || !cache.add(prefixes)) { - continue; - } - - final WordBuilder pathBuilder = new WordBuilder<>(); - for (TransitionEdge e : path) { - pathBuilder.append(e.getInput()); - } - - /* - * in case of non-strongly connected automata test case might not be possible as a path between 2 states - * might not exist - */ - boolean possibleTestCase = true; - for (int index = 0; index < comb.size() - 1; index++) { - final List> pathBetweenStates = - apsp.getShortestPath(comb.get(index), comb.get(index + 1)); + if (path == null || !cache.add(prefixes)) { + continue; + } - if (pathBetweenStates == null || pathBetweenStates.isEmpty()) { - possibleTestCase = false; - break; - } + final WordBuilder pathBuilder = new WordBuilder<>(); + for (TransitionEdge e : path) { + pathBuilder.append(e.getInput()); + } - for (TransitionEdge t : pathBetweenStates) { - pathBuilder.append(t.getInput()); - } + /* + * in case of non-strongly connected automata test case might not be possible as a path between 2 states + * might not exist + */ + boolean possibleTestCase = true; + for (int index = 0; index < comb.size() - 1; index++) { + final List> pathBetweenStates = + apsp.getShortestPath(comb.get(index), comb.get(index + 1)); + + if (pathBetweenStates == null || pathBetweenStates.isEmpty()) { + possibleTestCase = false; + break; } - if (possibleTestCase) { - pathBuilder.append(RandomUtil.sample(random, alphabet, randomWalkLen)); - super.nextValue = pathBuilder.toWord(); - return true; + for (TransitionEdge t : pathBetweenStates) { + pathBuilder.append(t.getInput()); } } - return false; - } - - /** - * Compute all-pair-shortest-paths lazily, in case this iterator is never queried. - * - * @return the all-pair-shortest-paths result - */ - private APSPResult> getAPSP() { - if (this.apsp == null) { - this.apsp = Graphs.findAPSP(automaton.transitionGraphView(alphabet)); + if (possibleTestCase) { + pathBuilder.append(RandomUtil.sample(random, alphabet, randomWalkLen)); + super.nextValue = pathBuilder.toWord(); + return true; } - return this.apsp; } + + return false; } /** diff --git a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java index 83506d190..506a07604 100644 --- a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java +++ b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java @@ -33,6 +33,7 @@ import net.automatalib.common.util.collection.AbstractTwoLevelIterator; import net.automatalib.common.util.collection.CollectionUtil; import net.automatalib.common.util.collection.IterableUtil; +import net.automatalib.common.util.collection.IteratorUtil; import net.automatalib.common.util.random.RandomUtil; import net.automatalib.util.graph.Graphs; import net.automatalib.util.graph.apsp.APSPResult; @@ -48,6 +49,11 @@ * This iterator selects test cases based on k-way transitions coverage. It does that by generating random test words * and finding the smallest subset with the highest coverage. In other words, this iterator generates test words 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 IteratorUtil#concat(Iterator[]) combine} this generator with other + * generators or limit the number of generated test cases. * * @param * automaton state type diff --git a/util/src/test/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIteratorTest.java b/util/src/test/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIteratorTest.java index 1fcd94561..47ac1fcb9 100644 --- a/util/src/test/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIteratorTest.java +++ b/util/src/test/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIteratorTest.java @@ -71,10 +71,8 @@ public void testSingleStateAutomaton(CombinationMethod method) { method)); // check that the first 'length' queries are the randomly generated ones. - Assert.assertTrue(tests.size() >= length); - for (Word t : tests.subList(0, length)) { - Assert.assertEquals(t.size(), length); - } + Assert.assertEquals(tests.size(), 1); + Assert.assertEquals(tests.get(0).length(), length); } @Test(dataProvider = "methods") From ae173dfa2887cd378e8ce2aa1fba96193519a8ca Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Mon, 15 Dec 2025 17:52:18 +0100 Subject: [PATCH 8/9] cleanups * add documentation * add tests * improve performance * declutter type variables --- .../util/collection/IteratableUtilTest.java | 22 +++ .../KWayStateCoverTestsIterator.java | 74 ++++++---- .../KWayTransitionCoverTestsIterator.java | 138 ++++++++---------- .../util/automaton/cover/Covers.java | 96 ++++++++---- 4 files changed, 191 insertions(+), 139 deletions(-) diff --git a/commons/util/src/test/java/net/automatalib/common/util/collection/IteratableUtilTest.java b/commons/util/src/test/java/net/automatalib/common/util/collection/IteratableUtilTest.java index a8c7d6c13..073f53fa3 100644 --- a/commons/util/src/test/java/net/automatalib/common/util/collection/IteratableUtilTest.java +++ b/commons/util/src/test/java/net/automatalib/common/util/collection/IteratableUtilTest.java @@ -16,7 +16,11 @@ package net.automatalib.common.util.collection; import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; import java.util.List; +import java.util.NoSuchElementException; +import java.util.Set; import org.testng.Assert; import org.testng.annotations.Test; @@ -34,4 +38,22 @@ public void testConcat() { Assert.assertEquals(concat1, Arrays.asList(1, 2, 4, 3)); Assert.assertEquals(concat2, Arrays.asList(4, 3, 1, 2)); } + + @Test + public void testExhaustiveness() { + + Set iterable = Collections.singleton(1); + Iterator> iter1 = IterableUtil.cartesianProduct(iterable).iterator(); + Iterator> iter2 = IterableUtil.allTuples(iterable, 1, 1).iterator(); + + // consume iterators + IteratorUtil.size(iter1); + IteratorUtil.size(iter2); + + Assert.assertFalse(iter1.hasNext()); + Assert.assertThrows(NoSuchElementException.class, iter1::next); + + Assert.assertFalse(iter2.hasNext()); + Assert.assertThrows(NoSuchElementException.class, iter2::next); + } } diff --git a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java index f90bc2917..f5b2da3af 100644 --- a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java +++ b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java @@ -18,21 +18,24 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Random; import java.util.Set; +import java.util.stream.Stream; -import net.automatalib.automaton.UniversalDeterministicAutomaton; -import net.automatalib.automaton.graph.TransitionEdge; +import net.automatalib.automaton.DeterministicAutomaton; import net.automatalib.common.util.HashUtil; import net.automatalib.common.util.collection.AbstractSimplifiedIterator; import net.automatalib.common.util.collection.CollectionUtil; import net.automatalib.common.util.collection.IteratorUtil; +import net.automatalib.common.util.mapping.Mapping; +import net.automatalib.common.util.mapping.Mappings; import net.automatalib.common.util.random.RandomUtil; -import net.automatalib.util.graph.Graphs; -import net.automatalib.util.graph.apsp.APSPResult; +import net.automatalib.util.automaton.cover.Covers; import net.automatalib.word.Word; import net.automatalib.word.WordBuilder; import org.checkerframework.checker.nullness.qual.Nullable; @@ -48,31 +51,37 @@ * 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 IteratorUtil#concat(Iterator[]) combine} this generator with other - * generators or limit the number of generated test cases. + * generators or {@link Stream#limit(long) limit} the number of generated test cases. * * @param * automaton state type * @param * input symbol type - * @param - * transition type * @param * automaton type */ -public class KWayStateCoverTestsIterator> +public class KWayStateCoverTestsIterator> extends AbstractSimplifiedIterator> { - public static final int DEFAULT_R_WALK_LEN = 20; + /** + * The default value of k used in the k-way combinations/permutations. + */ public static final int DEFAULT_K = 2; + /** + * The default length of random walks performed at the end of each combination/permutation. + */ + public static final int DEFAULT_R_WALK_LEN = 20; + + private final A automaton; private final List alphabet; private final Random random; private final int randomWalkLen; private final Iterator> combIter; - private final Set>>> cache; + private final Set>> cache; private final @Nullable S initial; - private final APSPResult> apsp; + private final Map>> apsp; /** * Convenience constructor which uses a fresh {@code random} object. @@ -82,15 +91,16 @@ public class KWayStateCoverTestsIterator inputs) { this(automaton, inputs, new Random()); } /** - * Convenience constructor. Uses {@code k=2}, {@code randomWalkLen = 20}, and - * {@code method = CombinationMethod.PERMUTATIONS}. + * Convenience constructor. Uses k = {@value #DEFAULT_K}, randomWalkLen = + * {@value #DEFAULT_R_WALK_LEN}, and + * method = {@link CombinationMethod#PERMUTATIONS}. * * @param automaton * the automaton for which to generate test cases @@ -99,8 +109,7 @@ public KWayStateCoverTestsIterator(A automaton, Collection inputs) * @param random * the random number generator to use * - * @see #KWayStateCoverTestsIterator(UniversalDeterministicAutomaton, Collection, Random, int, int, - * CombinationMethod) + * @see #KWayStateCoverTestsIterator(DeterministicAutomaton, Collection, Random, int, int, CombinationMethod) */ public KWayStateCoverTestsIterator(A automaton, Collection inputs, Random random) { this(automaton, inputs, random, DEFAULT_R_WALK_LEN, DEFAULT_K, CombinationMethod.PERMUTATIONS); @@ -116,9 +125,9 @@ public KWayStateCoverTestsIterator(A automaton, Collection inputs, * @param random * the random number generator to use * @param randomWalkLen - * length of random walk performed at the end of each combination/permutation + * length of random walks performed at the end of each combination/permutation * @param k - * k value used for k-wise combinations/permutations of states + * k value used for k-way combinations/permutations of states * @param method * the method for computing combinations */ @@ -128,20 +137,23 @@ public KWayStateCoverTestsIterator(A automaton, int randomWalkLen, int k, CombinationMethod method) { + this.automaton = automaton; this.alphabet = CollectionUtil.randomAccessList(inputs); this.random = random; this.randomWalkLen = randomWalkLen; this.cache = new HashSet<>(); - this.apsp = Graphs.findAPSP(automaton.transitionGraphView(alphabet)); this.initial = automaton.getInitialState(); if (this.initial == null) { this.combIter = Collections.emptyIterator(); + this.apsp = Collections.emptyMap(); } else { final List states = new ArrayList<>(automaton.getStates()); Collections.shuffle(states, random); this.combIter = method.getCombinations(states, Math.min(k, automaton.size())); + this.apsp = new HashMap<>(HashUtil.capacity(automaton.size())); + this.apsp.put(this.initial, Covers.cover(automaton, inputs, this.initial, w -> {}, w -> {})); } } @@ -151,13 +163,13 @@ protected boolean calculateNext() { while (combIter.hasNext()) { final List comb = combIter.next(); - final Set>> prefixes = new HashSet<>(HashUtil.capacity(comb.size())); + final Set> prefixes = new HashSet<>(HashUtil.capacity(comb.size())); - List> path = null; + Word path = null; assert initial != null; for (S c : comb) { - List> sp = apsp.getShortestPath(initial, c); + Word sp = apsp.getOrDefault(initial, Mappings.nullMapping()).get(c); if (sp != null) { prefixes.add(sp); if (path == null) { @@ -170,10 +182,7 @@ protected boolean calculateNext() { continue; } - final WordBuilder pathBuilder = new WordBuilder<>(); - for (TransitionEdge e : path) { - pathBuilder.append(e.getInput()); - } + final WordBuilder pathBuilder = new WordBuilder<>(path); /* * in case of non-strongly connected automata test case might not be possible as a path between 2 states @@ -181,17 +190,20 @@ protected boolean calculateNext() { */ boolean possibleTestCase = true; for (int index = 0; index < comb.size() - 1; index++) { - final List> pathBetweenStates = - apsp.getShortestPath(comb.get(index), comb.get(index + 1)); + final Word pathBetweenStates = apsp.computeIfAbsent(comb.get(index), + k -> Covers.cover(automaton, + alphabet, + k, + w -> {}, + w -> {})) + .get(comb.get(index + 1)); if (pathBetweenStates == null || pathBetweenStates.isEmpty()) { possibleTestCase = false; break; } - for (TransitionEdge t : pathBetweenStates) { - pathBuilder.append(t.getInput()); - } + pathBuilder.append(pathBetweenStates); } if (possibleTestCase) { diff --git a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java index 506a07604..153ea8082 100644 --- a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java +++ b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java @@ -25,18 +25,18 @@ import java.util.Objects; import java.util.Random; import java.util.Set; +import java.util.stream.Stream; -import net.automatalib.automaton.UniversalDeterministicAutomaton; -import net.automatalib.automaton.graph.TransitionEdge; +import net.automatalib.automaton.DeterministicAutomaton; import net.automatalib.common.util.HashUtil; import net.automatalib.common.util.collection.AbstractSimplifiedIterator; import net.automatalib.common.util.collection.AbstractTwoLevelIterator; import net.automatalib.common.util.collection.CollectionUtil; import net.automatalib.common.util.collection.IterableUtil; import net.automatalib.common.util.collection.IteratorUtil; +import net.automatalib.common.util.mapping.Mapping; import net.automatalib.common.util.random.RandomUtil; -import net.automatalib.util.graph.Graphs; -import net.automatalib.util.graph.apsp.APSPResult; +import net.automatalib.util.automaton.cover.Covers; import net.automatalib.word.Word; import net.automatalib.word.WordBuilder; import org.checkerframework.checker.nullness.qual.Nullable; @@ -53,25 +53,42 @@ * 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 IteratorUtil#concat(Iterator[]) combine} this generator with other - * generators or limit the number of generated test cases. + * generators or {@link Stream#limit(long) limit} the number of generated test cases. * * @param * automaton state type * @param * input symbol type - * @param - * transition type * @param * automaton type */ -public class KWayTransitionCoverTestsIterator> +public class KWayTransitionCoverTestsIterator> implements Iterator> { - public static final int DEFAULT_R_WALK_LEN = 10; - public static final int DEFAULT_NUM_GEN_PATHS = 1_000; + /** + * The default value of k used in the k-way computations. + */ + public static final int DEFAULT_K = 2; + + /** + * The default for the maximum step size of {@link GenerationMethod#RANDOM randomly}-generated paths. + */ public static final int DEFAULT_MAX_PATH_LENGTH = 50; + + /** + * The default (unlimited) threshold for the number of steps after which no more new test words will be generated. + */ public static final int DEFAULT_MAX_NUM_STEPS = 0; - public static final int DEFAULT_K = 2; + + /** + * The default number of {@link GenerationMethod#RANDOM randomly}-generated tests used to find the optimal subset. + */ + public static final int DEFAULT_NUM_GEN_PATHS = 1_000; + + /** + * The default number of steps that are appended to {@link GenerationMethod#PREFIX prefix}-generated paths. + */ + public static final int DEFAULT_R_WALK_LEN = 10; private final A automaton; private final List alphabet; @@ -93,16 +110,18 @@ public class KWayTransitionCoverTestsIterator inputs) { this(automaton, inputs, new Random()); } /** - * Convenience constructor. Uses {@code randomWalkLen=10}, {@code numGeneratePaths = 1_000}, - * {@code maxPathLen = 50}, {@code maxNumberOfSteps = 0}, {@code k = 2}, - * {@code optimizationMetric = OptimizationMetric.STEPS}, and {@code generationMethod = GenerationMethod.RANDOM}. + * Convenience constructor. Uses randomWalkLen = {@value #DEFAULT_R_WALK_LEN}, numGeneratePaths = + * {@value #DEFAULT_NUM_GEN_PATHS}, maxPathLen = {@value #DEFAULT_MAX_PATH_LENGTH}, + * maxNumberOfSteps = {@value #DEFAULT_MAX_NUM_STEPS}, k = {@value DEFAULT_K}, + * optimizationMetric = {@link OptimizationMetric#STEPS STEPS}, and generationMethod = + * {@link GenerationMethod#RANDOM RANDOM}. * * @param automaton * the automaton for which to generate test cases @@ -111,8 +130,8 @@ public KWayTransitionCoverTestsIterator(A automaton, Collection inp * @param random * the random number generator to use * - * @see #KWayTransitionCoverTestsIterator(UniversalDeterministicAutomaton, Collection, Random, int, int, int, int, - * int, OptimizationMetric, GenerationMethod) + * @see #KWayTransitionCoverTestsIterator(DeterministicAutomaton, Collection, Random, int, int, int, int, int, + * OptimizationMetric, GenerationMethod) */ public KWayTransitionCoverTestsIterator(A automaton, Collection inputs, Random random) { this(automaton, @@ -137,7 +156,7 @@ public KWayTransitionCoverTestsIterator(A automaton, Collection inp * @param random * the random number generator to use * @param randomWalkLen - * the number of steps that are added by {@link GenerationMethod#PREFIX prefix}-generated paths + * the number of steps that are appended to {@link GenerationMethod#PREFIX prefix}-generated paths * @param numGeneratePaths * number of {@link GenerationMethod#RANDOM randomly}-generated tests used to find the optimal subset * @param maxPathLen @@ -166,9 +185,9 @@ public KWayTransitionCoverTestsIterator(A automaton, this.alphabet = CollectionUtil.randomAccessList(inputs); this.random = random; + this.randomWalkLen = randomWalkLen; this.numGeneratePaths = numGeneratePaths; this.maxPathLen = maxPathLen; - this.randomWalkLen = randomWalkLen; this.maxNumberOfSteps = maxNumberOfSteps; this.k = Math.min(k, automaton.size()); this.optimizationMetric = optimizationMetric; @@ -280,7 +299,7 @@ private class GreedySetCoverIterator extends AbstractSimplifiedIterator> this.paths = generateRandomPaths(automaton, initial); this.covered = new HashSet<>(); this.stepCount = 0; - this.sizeOfUniverse = automaton.getStates().size() * (int) Math.pow(alphabet.size(), k); + this.sizeOfUniverse = Math.multiplyExact(automaton.getStates().size(), (int) Math.pow(alphabet.size(), k)); } @Override @@ -294,7 +313,7 @@ protected boolean calculateNext() { stepCount += path.steps.size(); super.nextValue = path.steps; - if (paths.isEmpty()){ + if (paths.isEmpty()) { computeNewPaths(); } return true; @@ -316,13 +335,11 @@ private void computeNewPaths() { private class PrefixStepsIterator extends AbstractTwoLevelIterator, Word> { - private final APSPResult> apsp; - private final S initial; + private final Mapping> accessSequences; PrefixStepsIterator(Iterator iterator, S initial) { super(iterator); - this.apsp = Graphs.findAPSP(automaton.transitionGraphView(alphabet)); - this.initial = initial; + this.accessSequences = Covers.cover(automaton, alphabet, initial, w -> {}, w -> {}); } @Override @@ -339,18 +356,16 @@ protected Iterator> l2Iterator(S state) { @Override protected Word combine(S state, List steps) { - final List> prefix = apsp.getShortestPath(initial, state); + final Word prefix = accessSequences.get(state); if (prefix == null) { return Word.epsilon(); } - final WordBuilder wb = new WordBuilder<>(); - for (TransitionEdge edge : prefix) { - wb.append(edge.getInput()); - } + final WordBuilder wb = new WordBuilder<>(prefix.length() + steps.size() + randomWalkLen); - wb.addAll(steps); - wb.addAll(RandomUtil.sample(random, alphabet, randomWalkLen)); + wb.append(prefix); + wb.append(steps); + wb.append(RandomUtil.sample(random, alphabet, randomWalkLen)); return wb.toWord(); } @@ -386,17 +401,8 @@ private int computeHashCode(@Nullable S startState, @Nullable S endState, Word that = (KWayTransition) o; - return Objects.equals(startState, that.startState) && Objects.equals(endState, that.endState) && - Objects.equals(steps, that.steps); + return o == this || o instanceof KWayTransition that && Objects.equals(startState, that.startState) && + Objects.equals(endState, that.endState) && steps.equals(that.steps); } @Override @@ -405,37 +411,7 @@ public int hashCode() { } } - private static final class Path { - - private final Word steps; - private final Set> kWayTransitions; - - Path(Word steps, Set> kWayTransitions) { - this.steps = steps; - this.kWayTransitions = kWayTransitions; - } - - @Override - public boolean equals(@Nullable Object o) { - if (o == this) { - return true; - } - - if (o == null || getClass() != o.getClass()) { - return false; - } - - final Path path = (Path) o; - return Objects.equals(steps, path.steps) && Objects.equals(kWayTransitions, path.kWayTransitions); - } - - @Override - public int hashCode() { - int result = Objects.hashCode(steps); - result = 31 * result + Objects.hashCode(kWayTransitions); - return result; - } - } + private record Path(Word steps, Set> kWayTransitions) {} /** * Method by which the prefixes of test words should be generated. @@ -446,8 +422,8 @@ public enum GenerationMethod { */ RANDOM { @Override - > Iterator> getIterator( - KWayTransitionCoverTestsIterator self, + > Iterator> getIterator( + KWayTransitionCoverTestsIterator self, S initial) { return self.new GreedySetCoverIterator(initial); } @@ -457,15 +433,15 @@ public enum GenerationMethod { */ PREFIX { @Override - > Iterator> getIterator( - KWayTransitionCoverTestsIterator self, + > Iterator> getIterator( + KWayTransitionCoverTestsIterator self, S initial) { return self.generatePrefixSteps(self.automaton, initial); } }; - abstract > Iterator> getIterator( - KWayTransitionCoverTestsIterator self, + abstract > Iterator> getIterator( + KWayTransitionCoverTestsIterator self, S initial); } @@ -489,7 +465,7 @@ Comparator> getPathComparator(Set> covere QUERIES { @Override Comparator> getPathComparator(Set> covered) { - return Comparator.comparingDouble(p -> sizeOfSetDifference(p.kWayTransitions, covered)); + return Comparator.comparingInt(p -> sizeOfSetDifference(p.kWayTransitions, covered)); } }; diff --git a/util/src/main/java/net/automatalib/util/automaton/cover/Covers.java b/util/src/main/java/net/automatalib/util/automaton/cover/Covers.java index 26653898e..e1f870065 100644 --- a/util/src/main/java/net/automatalib/util/automaton/cover/Covers.java +++ b/util/src/main/java/net/automatalib/util/automaton/cover/Covers.java @@ -26,8 +26,10 @@ import net.automatalib.automaton.DeterministicAutomaton; import net.automatalib.common.util.HashUtil; +import net.automatalib.common.util.mapping.Mapping; import net.automatalib.common.util.mapping.MutableMapping; import net.automatalib.word.Word; +import org.checkerframework.checker.nullness.qual.Nullable; public final class Covers { @@ -48,13 +50,15 @@ private Covers() {} * the set of input symbols allowed in the cover sequences * @param states * the collection in which the sequences will be stored + * @param + * automaton state type * @param * input symbol type */ - public static void stateCover(DeterministicAutomaton automaton, - Collection inputs, - Collection> states) { - cover(automaton, inputs, states::add, w -> {}); + public static void stateCover(DeterministicAutomaton automaton, + Collection inputs, + Collection> states) { + cover(automaton, inputs, automaton.getInitialState(), states::add, w -> {}); } /** @@ -93,13 +97,15 @@ public static Iterator> stateCoverIterator(DeterministicAutomaton + * automaton state type * @param * input symbol type */ - public static void transitionCover(DeterministicAutomaton automaton, - Collection inputs, - Collection> transitions) { - cover(automaton, inputs, w -> {}, transitions::add); + public static void transitionCover(DeterministicAutomaton automaton, + Collection inputs, + Collection> transitions) { + cover(automaton, inputs, automaton.getInitialState(), w -> {}, transitions::add); } /** @@ -133,16 +139,18 @@ public static Iterator> transitionCoverIterator(DeterministicAutomat * the set of input symbols allowed in the cover sequences * @param cover * the collection in which the sequences will be stored + * @param + * automaton state type * @param * input symbol type * * @see #stateCover(DeterministicAutomaton, Collection, Collection) * @see #transitionCover(DeterministicAutomaton, Collection, Collection) */ - public static void structuralCover(DeterministicAutomaton automaton, - Collection inputs, - Collection> cover) { - cover(automaton, inputs, cover::add, cover::add); + public static void structuralCover(DeterministicAutomaton automaton, + Collection inputs, + Collection> cover) { + cover(automaton, inputs, automaton.getInitialState(), cover::add, cover::add); } /** @@ -156,35 +164,67 @@ public static void structuralCover(DeterministicAutomaton automaton * the collection in which the state cover sequences will be stored * @param transitions * the collection in which the transition cover sequences will be stored + * @param + * automaton state type * @param * input symbol type * * @see #stateCover(DeterministicAutomaton, Collection, Collection) * @see #transitionCover(DeterministicAutomaton, Collection, Collection) */ - public static void cover(DeterministicAutomaton automaton, - Collection inputs, - Collection> states, - Collection> transitions) { - cover(automaton, inputs, states::add, transitions::add); + public static void cover(DeterministicAutomaton automaton, + Collection inputs, + Collection> states, + Collection> transitions) { + cover(automaton, inputs, automaton.getInitialState(), states::add, transitions::add); } - private static void cover(DeterministicAutomaton automaton, - Collection inputs, - Consumer> states, - Consumer> transitions) { + /** + * Computes the state and transition covers for a given automaton beginning in the given state. + *

+ * A state cover is a set C of input sequences, such that for each state s of an automaton, there + * exists an input sequence in C that transitions the automaton from its initial state to state s. + *

+ * A transition cover is a set C of input sequences, such that for each state s and each input symbol + * i of an automaton, there exists an input sequence in C that starts from the initial state of the + * automaton and ends with the transition that applies i to state s. + *

+ * Note: if restrictions on the {@code inputs} parameter do not allow to reach certain states or transitions, the + * computed covers are not complete. + * + * @param automaton + * the automaton for which the covers should be computed + * @param inputs + * the set of input symbols allowed in the cover sequences + * @param start + * the state from which to begin the computation of overs + * @param states + * a consumer that accepts the state cover sequences + * @param transitions + * a consumer that accepts the transition cover sequences + * @param + * automaton state type + * @param + * input symbol type + * + * @return a mapping from automaton states to their access sequences + */ + public static Mapping> cover(DeterministicAutomaton automaton, + Collection inputs, + @Nullable S start, + Consumer> states, + Consumer> transitions) { - S init = automaton.getInitialState(); + MutableMapping> reach = automaton.createStaticStateMapping(); - if (init == null) { - return; + if (start == null) { + return reach; } - MutableMapping> reach = automaton.createStaticStateMapping(); - reach.put(init, Word.epsilon()); + reach.put(start, Word.epsilon()); Queue bfsQueue = new ArrayDeque<>(); - bfsQueue.add(init); + bfsQueue.add(start); states.accept(Word.epsilon()); @@ -210,6 +250,8 @@ private static void cover(DeterministicAutomaton automaton, transitions.accept(succAs); } } + + return reach; } /** From 6c2161f03c522a1dd20aff5c23c4d37ee5db2828 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Mon, 15 Dec 2025 18:02:45 +0100 Subject: [PATCH 9/9] do not use special characters in documentation --- .../conformance/KWayTransitionCoverTestsIterator.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java index 153ea8082..960cf9aba 100644 --- a/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java +++ b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java @@ -162,7 +162,8 @@ public KWayTransitionCoverTestsIterator(A automaton, Collection inp * @param maxPathLen * the maximum step size of {@link GenerationMethod#RANDOM randomly}-generated paths * @param maxNumberOfSteps - * threshold for the number of steps after which no more new test words will be generated (<=0 = no limit) + * 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