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
+ * 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
+ * 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
+ * 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
+ * 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 {@link Stream#limit(long) limit} the number of generated test cases.
+ *
+ * @param
+ * 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.
+ * > {
+final class AllCombinationsIterator
> {
- private final Iterable extends T>[] iterables;
- private final Iterator extends T>[] iterators;
- private final List
> {
+
+ private final int n;
+ private final int[] indices;
+ private final int[] cycles;
+ private final int r;
+
+ private boolean initial;
+
+ AllPermutationsIterator(Collection extends T> 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..efb45b94f
--- /dev/null
+++ b/commons/util/src/main/java/net/automatalib/common/util/collection/CartesianProductIterator.java
@@ -0,0 +1,100 @@
+/* Copyright (C) 2013-2025 TU Dortmund University
+ * This file is part of AutomataLib
> {
+
+ private final Iterable extends T>[] iterables;
+ private final Iterator extends T>[] iterators;
+ private final List
> allCombintationsIterator(Collection extends T> 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
> allPermutationsIterator(Collection extends T> elements) {
+ return allPermutationsIterator(elements, elements.size());
+ }
+
+ /**
+ * Return {@code k}-length permutations of elements from the input collection.
+ *
> allPermutationsIterator(Collection extends T> 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
> allTuples(Iterable extends T> domain, int length) {
return allTuples(domain, length, length);
}
@@ -44,7 +60,7 @@ public static
> allTuples(Iterable extends T> 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
> cartesianProduct(Iterable
> lists = CollectionUtil.allPermutationsIterator(COLLECTION, k);
+ final List
> lists = CollectionUtil.allPermutationsIterator(COLLECTION, k);
+ final List
> lists = CollectionUtil.allPermutationsIterator(COLLECTION);
+ final List
> lists = CollectionUtil.allCombintationsIterator(COLLECTION, k);
+ final List
> lists = CollectionUtil.allCombintationsIterator(COLLECTION, k);
+ final List
> lists = CollectionUtil.allCombintationsIterator(COLLECTION, k);
+ final List
> 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/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
> 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
> 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
new file mode 100644
index 000000000..f5b2da3af
--- /dev/null
+++ b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayStateCoverTestsIterator.java
@@ -0,0 +1,248 @@
+/* Copyright (C) 2013-2025 TU Dortmund University
+ * This file is part of AutomataLib
+ * automaton state type
+ * @param
+ * input symbol type
+ * @param
+ * automaton type
+ */
+public class KWayStateCoverTestsIterator>
+ extends AbstractSimplifiedIterator> combIter;
+ private final Set
>> apsp;
+
+ /**
+ * 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(DeterministicAutomaton, Collection, Random)
+ */
+ public KWayStateCoverTestsIterator(A automaton, Collection extends I> inputs) {
+ this(automaton, inputs, new Random());
+ }
+
+ /**
+ * 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
+ * @param inputs
+ * the inputs to consider for test case generation
+ * @param random
+ * the random number generator to use
+ *
+ * @see #KWayStateCoverTestsIterator(DeterministicAutomaton, Collection, Random, int, int, CombinationMethod)
+ */
+ public KWayStateCoverTestsIterator(A automaton, Collection extends I> inputs, Random random) {
+ 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 walks performed at the end of each combination/permutation
+ * @param k
+ * k value used for k-way combinations/permutations of states
+ * @param method
+ * the method for computing combinations
+ */
+ public KWayStateCoverTestsIterator(A automaton,
+ Collection extends I> inputs,
+ Random random,
+ 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.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 -> {}));
+ }
+
+ }
+
+ @Override
+ protected boolean calculateNext() {
+
+ while (combIter.hasNext()) {
+ final List comb = combIter.next();
+ final Set Iterator> getCombinations(List
states, int k) {
+ 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 CollectionUtil.allPermutationsIterator(states, k);
+ }
+ };
+
+ 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..960cf9aba
--- /dev/null
+++ b/util/src/main/java/net/automatalib/util/automaton/conformance/KWayTransitionCoverTestsIterator.java
@@ -0,0 +1,475 @@
+/* Copyright (C) 2013-2025 TU Dortmund University
+ * This file is part of AutomataLib