diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ff89db77..d0a330f5f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: mvn -B install -DskipTests cd $GITHUB_WORKSPACE - name: Run Maven - run: mvn -B install site -Pintegration-tests,code-analysis,bundles,jlink + run: mvn -B install site -Pintegration-tests,code-analysis,bundles platform-integration: name: "Platform Integration (JDK: ${{ matrix.jdk }}, OS: ${{ matrix.os }})" needs: [ tests-and-analysis ] @@ -81,7 +81,7 @@ jobs: mvn -B install -DskipTests cd $GITHUB_WORKSPACE - name: Run Maven - run: mvn -B install -Pjlink + run: mvn -B install -Pcli coverage: name: "Coverage" needs: [ platform-integration ] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..2615c3306 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,35 @@ +name: Attach artifacts to GitHub release +on: + release: + types: [published] +jobs: + publish: + name: "Build and attach artifacts" + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-22.04, windows-2022, macos-15-intel, macos-15 ] + permissions: + contents: write + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-java@v5 + with: + java-version: '25' + distribution: 'temurin' + cache: 'maven' + - name: Build AutomataLib # can be removed on actual stable releases + shell: bash + run: | + git clone -b develop --single-branch https://github.com/LearnLib/automatalib.git ${HOME}/automatalib-git + cd ${HOME}/automatalib-git + mvn -B install -DskipTests + cd $GITHUB_WORKSPACE + - name: Build + run: mvn -B package -DskipTests -Pcli + - name: Release + uses: softprops/action-gh-release@v3 + with: + working_directory: cli/target + files: learnlib-cli-*.zip + fail_on_unmatched_files: true diff --git a/CHANGELOG.md b/CHANGELOG.md index cc046ef9d..a4ec2b907 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added +* GitHub releases now provide `learnlib-cli` artifacts for using LearnLib via the command-line interface without the need for Java or Maven. * Added a new (L*-based) learning algorithm for *Mealy machines with local timers* (MMLTs), including support for parallel queries, caching, and conformance testing (thanks to [Paul Kogel](https://github.com/pdev55)). * Added the Ls active learning algorithm for Mealy machines (thanks to [Wolffhardt Schwabe](https://github.com/stateMachinist)). * Added an `EarlyExitEQOracle` which for a given `AdaptiveMembershipOracle` and `TestWordGenerator` stops the evaluation of (potentially long) Mealy-based equivalence tests as soon as a mismatch with the hypothesis is detected, potentially improving the symbol performance of the given equivalence oracle. @@ -21,12 +22,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * Statistics collection has received a major rework. Previously, classes would implement the `StatisticCollector` interface and return a `StatisticData` object which 1) only allows for describing a very limited amount of data, and 2) requires you to keep track of all the objects that collect data. This approach has been *replaced* by a new `StatisticsService`. Instances of this service can be obtained similar to a logger via `Statistics.getService()` and require you to provide an implementation of this service on the classpath (a default one is provided by the `learnlib-statistics` module). The new service allows arbitrary components to collect various data which can be conveniently extracted based on the new `StatisticsKey`s used by the components. For more details on advanced scenarios (such as multi-threaded benchmarking), see the documentation of the respective classes. While this may require you to adjust the way you are collecting statistics, all functionality from beforehand should still be available. * `SimpleProfiler` has been replaced by the new clock-based statistics. * Most learners now more rigorously implement the `LearningAlgorithm` contract that, e.g., duplicate invocations of `startLearning` or calling `refineHypothesis` / `getHypothesisModel` before `startLearning` throw `IllegalStateException`s. +* `Experiment` now has type variables for the input symbol type and output domain type. +* `{DFA,Mealy,Moore}Experiment` have been moved to the `de.learnlib.util` package. * The `generateTestWords` method of `AbstractTestWordEQOracle` now needs to be public. * The classes of `de.learnlib.testsupport.it.learner` have been split into the packages `de.learnlib.testsupport.it{,testcase,util,variant}` in the same module (`de.learnlib.testsupport:learnlib-learner-it-support`). ### Removed -* All *adapters* from the `learnlib-procedural` learner have been removed to due main learners implementing `AccessSequenceTransformer` now. Use the constructors of the main learners instead. +* All *adapters* from the `learnlib-procedural` learner have been removed due to main learners implementing `AccessSequenceTransformer` now. Use the constructors of the main learners instead. ### Fixed diff --git a/algorithms/active/aaar/src/test/java/de/learnlib/algorithm/aaar/AbstractAAARTest.java b/algorithms/active/aaar/src/test/java/de/learnlib/algorithm/aaar/AbstractAAARTest.java index 6b4998252..5988b8ce3 100644 --- a/algorithms/active/aaar/src/test/java/de/learnlib/algorithm/aaar/AbstractAAARTest.java +++ b/algorithms/active/aaar/src/test/java/de/learnlib/algorithm/aaar/AbstractAAARTest.java @@ -52,12 +52,12 @@ public void testAbstractHypothesisEquivalence() { final WpMethodTestsIterator iter = new WpMethodTestsIterator<>(automaton, alphabet); final List> testCases = IteratorUtil.list(iter); - final SampleSetEQOracle eqo = new SampleSetEQOracle<>(); - eqo.addAll(new SimulatorOracle<>(automaton), testCases); + final SampleSetEQOracle eqo = + new SampleSetEQOracle().addAll(new SimulatorOracle<>(automaton), testCases); final LearningAlgorithm learner = new TranslatingLearnerWrapper<>((AbstractAAARLearner) aaarLearner); - final Experiment exp = new Experiment<>(learner, eqo, alphabet); + final Experiment exp = new Experiment<>(learner, eqo, alphabet); exp.run(); diff --git a/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner/ADTLearner.java b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner/ADTLearner.java index d236176ec..f78b63b34 100644 --- a/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner/ADTLearner.java +++ b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner/ADTLearner.java @@ -60,6 +60,7 @@ import de.learnlib.query.DefaultQuery; import de.learnlib.tooling.annotation.builder.GenerateBuilder; import de.learnlib.util.MQUtil; +import de.learnlib.util.mealy.Adaptive2MembershipWrapper; import net.automatalib.alphabet.Alphabet; import net.automatalib.alphabet.SupportsGrowingAlphabet; import net.automatalib.automaton.transducer.MealyMachine; @@ -104,12 +105,26 @@ public class ADTLearner implements LearningAlgorithm.MealyLearner, private ADTHypothesis hypothesis; private ADT, I, O> adt; + public ADTLearner(Alphabet alphabet, AdaptiveMembershipOracle oracle) { + this(alphabet, + oracle, + BuilderDefaults.leafSplitter(), + BuilderDefaults.adtExtender(), + BuilderDefaults.subtreeReplacer()); + } + public ADTLearner(Alphabet alphabet, AdaptiveMembershipOracle oracle, LeafSplitter leafSplitter, ADTExtender adtExtender, SubtreeReplacer subtreeReplacer) { - this(alphabet, oracle, leafSplitter, adtExtender, subtreeReplacer, true, LocalSuffixFinders.RIVEST_SCHAPIRE); + this(alphabet, + oracle, + leafSplitter, + adtExtender, + subtreeReplacer, + BuilderDefaults.useObservationTree(), + BuilderDefaults.suffixFinder()); } @GenerateBuilder(defaults = BuilderDefaults.class) diff --git a/algorithms/active/adt/src/test/java/de/learnlib/algorithm/adt/it/ADTIT.java b/algorithms/active/adt/src/test/java/de/learnlib/algorithm/adt/it/ADTIT.java index a4f2fc19a..aca5a0198 100644 --- a/algorithms/active/adt/src/test/java/de/learnlib/algorithm/adt/it/ADTIT.java +++ b/algorithms/active/adt/src/test/java/de/learnlib/algorithm/adt/it/ADTIT.java @@ -47,7 +47,7 @@ import de.learnlib.testsupport.MQ2AQWrapper; import de.learnlib.testsupport.it.AbstractMealyLearnerIT; import de.learnlib.testsupport.it.variant.LearnerVariantList; -import de.learnlib.util.Experiment.MealyExperiment; +import de.learnlib.util.MealyExperiment; import net.automatalib.alphabet.Alphabet; import net.automatalib.automaton.transducer.impl.CompactMealy; import net.automatalib.exception.FormatException; diff --git a/algorithms/active/lambda/src/test/java/de/learnlib/algorithm/lambda/AbstractCounterexampleQueueTest.java b/algorithms/active/lambda/src/test/java/de/learnlib/algorithm/lambda/AbstractCounterexampleQueueTest.java index 001c563e7..16aab8bc3 100644 --- a/algorithms/active/lambda/src/test/java/de/learnlib/algorithm/lambda/AbstractCounterexampleQueueTest.java +++ b/algorithms/active/lambda/src/test/java/de/learnlib/algorithm/lambda/AbstractCounterexampleQueueTest.java @@ -19,7 +19,7 @@ import de.learnlib.oracle.MembershipOracle.DFAMembershipOracle; import de.learnlib.oracle.equivalence.SampleSetEQOracle; import de.learnlib.oracle.membership.DFASimulatorOracle; -import de.learnlib.util.Experiment.DFAExperiment; +import de.learnlib.util.Experiment; import net.automatalib.alphabet.Alphabet; import net.automatalib.alphabet.impl.Alphabets; import net.automatalib.automaton.fsa.DFA; @@ -80,7 +80,8 @@ public void testPop() { final Word b = new WordBuilder<>('b', 9).toWord(); eqOracle.addAll(mqOracle, Word.fromWords(b, a, b, a, b, a, b, a)); - final DFAExperiment experiment = new DFAExperiment<>(learner, eqOracle, alphabet); + final Experiment, Character, Boolean> experiment = + new Experiment<>(learner, eqOracle, alphabet); experiment.run(); final DFA result = experiment.getFinalHypothesis(); diff --git a/algorithms/active/lambda/src/test/java/de/learnlib/algorithm/lambda/lstar/mealy/it/LLambdaMealyIT.java b/algorithms/active/lambda/src/test/java/de/learnlib/algorithm/lambda/lstar/mealy/it/LLambdaMealyIT.java index 7aab7adea..668abd70b 100644 --- a/algorithms/active/lambda/src/test/java/de/learnlib/algorithm/lambda/lstar/mealy/it/LLambdaMealyIT.java +++ b/algorithms/active/lambda/src/test/java/de/learnlib/algorithm/lambda/lstar/mealy/it/LLambdaMealyIT.java @@ -30,7 +30,7 @@ import de.learnlib.query.DefaultQuery; import de.learnlib.testsupport.it.AbstractMealyLearnerIT; import de.learnlib.testsupport.it.variant.LearnerVariantList.MealyLearnerVariantList; -import de.learnlib.util.Experiment.MealyExperiment; +import de.learnlib.util.MealyExperiment; import de.learnlib.util.mealy.MealyUtil; import net.automatalib.alphabet.Alphabet; import net.automatalib.automaton.transducer.MealyMachine; diff --git a/algorithms/active/nlstar/src/test/java/de/learnlib/algorithm/nlstar/NLStarTest.java b/algorithms/active/nlstar/src/test/java/de/learnlib/algorithm/nlstar/NLStarTest.java index 1b09dc85f..e4af83c0e 100644 --- a/algorithms/active/nlstar/src/test/java/de/learnlib/algorithm/nlstar/NLStarTest.java +++ b/algorithms/active/nlstar/src/test/java/de/learnlib/algorithm/nlstar/NLStarTest.java @@ -62,7 +62,8 @@ public void testIssue70() { final NLStarLearner learner = new NLStarLearner<>(alphabet, mqOracle); - final Experiment> experiment = new Experiment<>(learner, eqOracle, alphabet); + final Experiment, Character, Boolean> experiment = + new Experiment<>(learner, eqOracle, alphabet); experiment.run(); final NFA hyp = experiment.getFinalHypothesis(); diff --git a/algorithms/active/observation-pack-vpa/src/test/java/de/learnlib/algorithm/observationpack/vpa/DTVisualizationTest.java b/algorithms/active/observation-pack-vpa/src/test/java/de/learnlib/algorithm/observationpack/vpa/DTVisualizationTest.java index 7c29f5b25..c3a062280 100644 --- a/algorithms/active/observation-pack-vpa/src/test/java/de/learnlib/algorithm/observationpack/vpa/DTVisualizationTest.java +++ b/algorithms/active/observation-pack-vpa/src/test/java/de/learnlib/algorithm/observationpack/vpa/DTVisualizationTest.java @@ -50,7 +50,7 @@ public DTVisualizationTest() { final SimulatorEQOracle eqo = new SimulatorEQOracle<>(vpa); this.learner = new OPLearnerVPA<>(alphabet, mqo, AcexAnalyzers.BINARY_SEARCH_FWD); - final Experiment> exp = new Experiment<>(learner, eqo, alphabet); + final Experiment, Character, Boolean> exp = new Experiment<>(learner, eqo, alphabet); exp.run(); } diff --git a/algorithms/active/procedural/src/test/java/de/learnlib/algorithm/procedural/sba/OptimizationsTest.java b/algorithms/active/procedural/src/test/java/de/learnlib/algorithm/procedural/sba/OptimizationsTest.java index 616fb65dc..c6a311c12 100644 --- a/algorithms/active/procedural/src/test/java/de/learnlib/algorithm/procedural/sba/OptimizationsTest.java +++ b/algorithms/active/procedural/src/test/java/de/learnlib/algorithm/procedural/sba/OptimizationsTest.java @@ -55,7 +55,7 @@ public void testOptimizations() { final SBALearner learner = new SBALearner<>(alphabet, mqo, TTTLearnerDFA::new); - final Experiment> experiment = new Experiment<>(learner, eqo, alphabet); + final Experiment, Character, Boolean> experiment = new Experiment<>(learner, eqo, alphabet); experiment.run(); diff --git a/algorithms/active/procedural/src/test/java/de/learnlib/algorithm/procedural/spmm/OptimizationsTest.java b/algorithms/active/procedural/src/test/java/de/learnlib/algorithm/procedural/spmm/OptimizationsTest.java index 7b110ce4b..bbda20446 100644 --- a/algorithms/active/procedural/src/test/java/de/learnlib/algorithm/procedural/spmm/OptimizationsTest.java +++ b/algorithms/active/procedural/src/test/java/de/learnlib/algorithm/procedural/spmm/OptimizationsTest.java @@ -57,7 +57,8 @@ public void testOptimizations() { final SPMMLearner learner = new SPMMLearner<>(alphabet, spmm.getErrorOutput(), mqo, TTTLearnerMealy::new); - final Experiment> experiment = new Experiment<>(learner, eqo, alphabet); + final Experiment, Character, Word> experiment = + new Experiment<>(learner, eqo, alphabet); experiment.run(); diff --git a/archetypes/basic/src/main/resources/archetype-resources/src/main/java/Example.java b/archetypes/basic/src/main/resources/archetype-resources/src/main/java/Example.java index 71ab667dd..03c58e9ff 100644 --- a/archetypes/basic/src/main/resources/archetype-resources/src/main/java/Example.java +++ b/archetypes/basic/src/main/resources/archetype-resources/src/main/java/Example.java @@ -10,7 +10,7 @@ import de.learnlib.oracle.equivalence.DFAWMethodEQOracle; import de.learnlib.oracle.membership.DFASimulatorOracle; import de.learnlib.statistic.Statistics; -import de.learnlib.util.Experiment.DFAExperiment; +import de.learnlib.util.DFAExperiment; import net.automatalib.alphabet.Alphabet; import net.automatalib.alphabet.impl.Alphabets; import net.automatalib.automaton.fsa.DFA; diff --git a/archetypes/complete/src/main/resources/archetype-resources/src/main/java/Example.java b/archetypes/complete/src/main/resources/archetype-resources/src/main/java/Example.java index 71ab667dd..03c58e9ff 100644 --- a/archetypes/complete/src/main/resources/archetype-resources/src/main/java/Example.java +++ b/archetypes/complete/src/main/resources/archetype-resources/src/main/java/Example.java @@ -10,7 +10,7 @@ import de.learnlib.oracle.equivalence.DFAWMethodEQOracle; import de.learnlib.oracle.membership.DFASimulatorOracle; import de.learnlib.statistic.Statistics; -import de.learnlib.util.Experiment.DFAExperiment; +import de.learnlib.util.DFAExperiment; import net.automatalib.alphabet.Alphabet; import net.automatalib.alphabet.impl.Alphabets; import net.automatalib.automaton.fsa.DFA; diff --git a/build-parent/pom.xml b/build-parent/pom.xml index 93cadfcdd..b6f18f236 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -80,6 +80,11 @@ limitations under the License. de/learnlib/oracle/property/Mealy*Oracle.class de/learnlib/oracle/property/DFA*Chain.class de/learnlib/oracle/property/Mealy*Chain.class + + + de/learnlib/util/DFAExperiment.class + de/learnlib/util/MealyExperiment.class + de/learnlib/util/MooreExperiment.class @@ -260,13 +265,7 @@ limitations under the License. true true - - + ${project.build.directory}/test-checkerframework org.checkerframework diff --git a/cli/pom.xml b/cli/pom.xml new file mode 100644 index 000000000..4d233dd2c --- /dev/null +++ b/cli/pom.xml @@ -0,0 +1,351 @@ + + + + 4.0.0 + + + de.learnlib + learnlib-build-parent + 0.19.0-SNAPSHOT + ../build-parent/pom.xml + + + learnlib-cli + + LearnLib :: CLI + + A module for building a standalone command-line application of LearnLib. Note that this artifact is not intended + as a library and therefore not deployed to Maven Central but instead provided as a direct download (e.g., from + GitHub releases). + + + + ${project.build.directory}/maven-jlink/default/bin/learnlib + + + + + + + de.learnlib + learnlib-api + + + de.learnlib + learnlib-cache + + + de.learnlib + learnlib-membership-oracles + + + de.learnlib + learnlib-parallelism + + + de.learnlib + learnlib-util + + + de.learnlib + learnlib-statistics + + + + + de.learnlib + learnlib-adt + + + de.learnlib + learnlib-dhc + + + de.learnlib + learnlib-equivalence-oracles + + + de.learnlib + learnlib-kearns-vazirani + + + de.learnlib + learnlib-lambda + + + de.learnlib + learnlib-lsharp + + + de.learnlib + learnlib-lstar + + + de.learnlib + learnlib-nlstar + + + de.learnlib + learnlib-observation-pack + + + de.learnlib + learnlib-observation-pack-vpa + + + de.learnlib + learnlib-procedural + + + de.learnlib + learnlib-sparse + + + de.learnlib + learnlib-ttt + + + de.learnlib + learnlib-ttt-vpa + + + + + net.automatalib + automata-api + + + net.automatalib + automata-commons-util + + + net.automatalib + automata-core + + + net.automatalib + automata-serialization-aut + + + net.automatalib + automata-serialization-ba + + + net.automatalib + automata-serialization-dot + + + net.automatalib + automata-serialization-learnlibv2 + + + net.automatalib + automata-serialization-mata + + + net.automatalib + automata-serialization-saf + + + net.automatalib + automata-serialization-taf + + + net.automatalib + automata-util + + + + ch.qos.logback + logback-classic + + + info.picocli + picocli + + + org.apache.fory + fory-core + + + org.checkerframework + checker-qual + + + org.slf4j + slf4j-api + + + + + org.mockito + mockito-core + + + org.testng + testng + + + + + + + src/main/resources + true + + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + + + + kr.motd.maven + os-maven-plugin + ${os-plugin.version} + + + + + + + cli + + + + maven-failsafe-plugin + + + check-binary + + integration-test + verify + + + + ${learnlib.binary.path} + + + **/CheckBinary.java + + + **/*IT.java + + + + + + + org.codehaus.mojo + license-maven-plugin + + test,provided,system + true + + + + add-third-party + + add-third-party + + + + download-licenses + + download-licenses + + + + + + org.apache.maven.plugins + maven-jlink-plugin + + + package + + jlink + + + false + + + ${project.build.directory}/generated-sources/license/ + THIRD-PARTY.txt + legal/third-party + + + ${project.build.directory}/generated-resources/licenses/ + legal/third-party/licenses + + + true + true + true + ${project.artifactId}-${project.version} + ${project.artifactId}-${project.version}-${os.detected.classifier} + learnlib=de.learnlib.cli/de.learnlib.cli.Application + + + + + + + + + jlink-on-java-24 + + [24,] + + + + + + org.apache.maven.plugins + maven-jlink-plugin + + + + + --add-opens=java.base/java.lang.invoke=org.apache.fory.core + + + + + + + + + windows + + + windows + + + + ${project.build.directory}/maven-jlink/default/bin/learnlib.bat + + + + diff --git a/cli/src/main/java/de/learnlib/cli/Application.java b/cli/src/main/java/de/learnlib/cli/Application.java new file mode 100644 index 000000000..82bf9e5fe --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/Application.java @@ -0,0 +1,70 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import de.learnlib.cli.option.Options; +import de.learnlib.cli.util.VersionProvider; +import org.slf4j.LoggerFactory; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; + +@Command(name = "learnlib", + mixinStandardHelpOptions = true, + versionProvider = VersionProvider.class, + resourceBundle = Application.PROPERTIES, + showDefaultValues = true, + showAtFileInUsageHelp = true, + descriptionHeading = "%nDescription:%n%n", + parameterListHeading = "%nParameters:%n", + optionListHeading = "%nOptions:%n", + description = "Runs an active automata learning process by invoking the provided SUL(s) to answer membership queries. For learning acceptor-based formalisms, the tool will use the exitcode of the binary to determine acceptance where an exitcode of 0 equals 'accept' and an exitcode unequal to 0 equals 'reject'. For learning transduction-based formalisms, the tool expects the SUL to emit an output that is transformed into individual symbols using the provided 'delimiter'. For additional information on the involved components of LearnLib, you may use the documentation available at https://learnlib.de/learnlib/maven-site/.") +public class Application implements Runnable { + + public static final String PROPERTIES = "application"; + + @Mixin + private Options options; + + public static void main(String[] args) { + CommandLine commandLine = new CommandLine(new Application()); + commandLine.setCaseInsensitiveEnumValuesAllowed(true); + System.exit(commandLine.execute(args)); + } + + @Override + public void run() { + setLogLevel(options); + options.type.runner(options).run(options); + } + + private void setLogLevel(Options options) { + if (options.verbosity != null) { + final Logger root = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); + + Level level; + if (options.verbosity.length == 1) { + level = Level.DEBUG; + } else { + level = Level.TRACE; + } + + root.setLevel(level); + } + } +} diff --git a/cli/src/main/java/de/learnlib/cli/adapter/ProceduralDFAAdapter.java b/cli/src/main/java/de/learnlib/cli/adapter/ProceduralDFAAdapter.java new file mode 100644 index 000000000..11eb41287 --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/adapter/ProceduralDFAAdapter.java @@ -0,0 +1,91 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.adapter; + +import de.learnlib.AccessSequenceTransformer; +import de.learnlib.algorithm.LearningAlgorithm.DFALearner; +import de.learnlib.algorithm.kv.dfa.KearnsVaziraniDFA; +import de.learnlib.algorithm.lambda.lstar.LLambdaDFA; +import de.learnlib.algorithm.lambda.ttt.dfa.TTTLambdaDFA; +import de.learnlib.algorithm.lstar.dfa.ExtensibleLStarDFA; +import de.learnlib.algorithm.malerpnueli.MalerPnueliDFA; +import de.learnlib.algorithm.observationpack.dfa.OPLearnerDFA; +import de.learnlib.algorithm.rivestschapire.RivestSchapireDFA; +import de.learnlib.algorithm.ttt.dfa.TTTLearnerDFA; +import de.learnlib.oracle.MembershipOracle; +import net.automatalib.alphabet.Alphabet; +import net.automatalib.alphabet.SupportsGrowingAlphabet; + +public interface ProceduralDFAAdapter + extends DFALearner, SupportsGrowingAlphabet, AccessSequenceTransformer { + + final class KearnsVaziraniDFAAdapter extends KearnsVaziraniDFA implements ProceduralDFAAdapter { + + public KearnsVaziraniDFAAdapter(Alphabet alphabet, MembershipOracle oracle) { + super(alphabet, oracle); + } + } + + final class LLambdaDFAAdapter extends LLambdaDFA implements ProceduralDFAAdapter { + + public LLambdaDFAAdapter(Alphabet alphabet, MembershipOracle oracle) { + super(alphabet, oracle); + } + } + + final class ExtensibleLStarDFAAdapter extends ExtensibleLStarDFA implements ProceduralDFAAdapter { + + public ExtensibleLStarDFAAdapter(Alphabet alphabet, MembershipOracle oracle) { + super(alphabet, oracle); + } + } + + final class MalerPnueliDFAAdapter extends MalerPnueliDFA implements ProceduralDFAAdapter { + + public MalerPnueliDFAAdapter(Alphabet alphabet, MembershipOracle oracle) { + super(alphabet, oracle); + } + } + + final class OPLearnerDFAAdapter extends OPLearnerDFA implements ProceduralDFAAdapter { + + public OPLearnerDFAAdapter(Alphabet alphabet, MembershipOracle oracle) { + super(alphabet, oracle); + } + } + + final class RivestSchapireDFAAdapter extends RivestSchapireDFA implements ProceduralDFAAdapter { + + public RivestSchapireDFAAdapter(Alphabet alphabet, MembershipOracle oracle) { + super(alphabet, oracle); + } + } + + final class TTTLearnerDFAAdapter extends TTTLearnerDFA implements ProceduralDFAAdapter { + + public TTTLearnerDFAAdapter(Alphabet alphabet, MembershipOracle oracle) { + super(alphabet, oracle); + } + } + + final class TTTLambdaDFAAdapter extends TTTLambdaDFA implements ProceduralDFAAdapter { + + public TTTLambdaDFAAdapter(Alphabet alphabet, MembershipOracle oracle) { + super(alphabet, oracle); + } + } + +} diff --git a/cli/src/main/java/de/learnlib/cli/adapter/ProceduralMealyAdapter.java b/cli/src/main/java/de/learnlib/cli/adapter/ProceduralMealyAdapter.java new file mode 100644 index 000000000..056951949 --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/adapter/ProceduralMealyAdapter.java @@ -0,0 +1,111 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.adapter; + +import de.learnlib.AccessSequenceTransformer; +import de.learnlib.algorithm.LearningAlgorithm.MealyLearner; +import de.learnlib.algorithm.dhc.mealy.MealyDHC; +import de.learnlib.algorithm.kv.mealy.KearnsVaziraniMealy; +import de.learnlib.algorithm.lambda.lstar.LLambdaMealy; +import de.learnlib.algorithm.lambda.ttt.mealy.TTTLambdaMealy; +import de.learnlib.algorithm.lstar.mealy.ExtensibleLStarMealy; +import de.learnlib.algorithm.malerpnueli.MalerPnueliMealy; +import de.learnlib.algorithm.observationpack.mealy.OPLearnerMealy; +import de.learnlib.algorithm.rivestschapire.RivestSchapireMealy; +import de.learnlib.algorithm.sparse.SparseLearner; +import de.learnlib.algorithm.ttt.mealy.TTTLearnerMealy; +import de.learnlib.oracle.MembershipOracle; +import net.automatalib.alphabet.Alphabet; +import net.automatalib.alphabet.SupportsGrowingAlphabet; +import net.automatalib.word.Word; + +public interface ProceduralMealyAdapter + extends MealyLearner, SupportsGrowingAlphabet, AccessSequenceTransformer { + + final class MealyDHCAdapter extends MealyDHC implements ProceduralMealyAdapter { + + public MealyDHCAdapter(Alphabet alphabet, MembershipOracle> oracle) { + super(alphabet, oracle); + } + } + + final class KearnsVaziraniMealyAdapter extends KearnsVaziraniMealy + implements ProceduralMealyAdapter { + + public KearnsVaziraniMealyAdapter(Alphabet alphabet, MembershipOracle> oracle) { + super(alphabet, oracle); + } + } + + final class LLambdaMealyAdapter extends LLambdaMealy implements ProceduralMealyAdapter { + + public LLambdaMealyAdapter(Alphabet alphabet, MembershipOracle> oracle) { + super(alphabet, oracle); + } + } + + final class ExtensibleLStarMealyAdapter extends ExtensibleLStarMealy + implements ProceduralMealyAdapter { + + public ExtensibleLStarMealyAdapter(Alphabet alphabet, MembershipOracle> oracle) { + super(alphabet, oracle); + } + } + + final class MalerPnueliMealyAdapter extends MalerPnueliMealy implements ProceduralMealyAdapter { + + public MalerPnueliMealyAdapter(Alphabet alphabet, MembershipOracle> oracle) { + super(alphabet, oracle); + } + } + + final class SparseLearnerAdapter extends SparseLearner implements ProceduralMealyAdapter { + + public SparseLearnerAdapter(Alphabet alphabet, MembershipOracle> oracle) { + super(alphabet, oracle); + } + } + + final class OPLearnerMealyAdapter extends OPLearnerMealy implements ProceduralMealyAdapter { + + public OPLearnerMealyAdapter(Alphabet alphabet, MembershipOracle> oracle) { + super(alphabet, oracle); + } + } + + final class RivestSchapireMealyAdapter extends RivestSchapireMealy + implements ProceduralMealyAdapter { + + public RivestSchapireMealyAdapter(Alphabet alphabet, MembershipOracle> oracle) { + super(alphabet, oracle); + } + } + + final class TTTLearnerMealyAdapter extends TTTLearnerMealy implements ProceduralMealyAdapter { + + public TTTLearnerMealyAdapter(Alphabet alphabet, MembershipOracle> oracle) { + super(alphabet, oracle); + } + } + + final class TTTLambdaMealyAdapter extends TTTLambdaMealy implements ProceduralMealyAdapter { + + public TTTLambdaMealyAdapter(Alphabet alphabet, MembershipOracle> oracle) { + super(alphabet, oracle); + } + } + +} diff --git a/cli/src/main/java/de/learnlib/cli/factory/AlphabetFactory.java b/cli/src/main/java/de/learnlib/cli/factory/AlphabetFactory.java new file mode 100644 index 000000000..3876816d8 --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/factory/AlphabetFactory.java @@ -0,0 +1,68 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.factory; + +import java.util.Objects; + +import de.learnlib.cli.option.Options; +import de.learnlib.cli.option.Symbols.ContextFreeSymbols; +import de.learnlib.cli.option.Symbols.RegularSymbols; +import net.automatalib.alphabet.Alphabet; +import net.automatalib.alphabet.ProceduralInputAlphabet; +import net.automatalib.alphabet.VPAlphabet; +import net.automatalib.alphabet.impl.Alphabets; +import net.automatalib.alphabet.impl.DefaultProceduralInputAlphabet; +import net.automatalib.alphabet.impl.DefaultVPAlphabet; + +public final class AlphabetFactory { + + private AlphabetFactory() { + // prevent instantiation + } + + public static Alphabet getRegularAlphabet(Options options) { + final RegularSymbols inputs = validateRegularSymbols(options); + return Alphabets.fromList(inputs.symbols); + } + + public static ProceduralInputAlphabet getProceduralAlphabet(Options options) { + final ContextFreeSymbols inputs = validateContextFreeSymbols(options); + if (inputs.returnSymbols.size() != 1) { + throw new IllegalArgumentException("Procedural systems require exactly one return symbol"); + } + return new DefaultProceduralInputAlphabet<>(Alphabets.fromList(inputs.internalSymbols), + Alphabets.fromList(inputs.callSymbols), + inputs.returnSymbols.get(0)); + } + + public static VPAlphabet getVPAlphabet(Options options) { + final ContextFreeSymbols inputs = validateContextFreeSymbols(options); + return new DefaultVPAlphabet<>(Alphabets.fromList(inputs.internalSymbols), + Alphabets.fromList(inputs.callSymbols), + Alphabets.fromList(inputs.returnSymbols)); + } + + private static RegularSymbols validateRegularSymbols(Options options) { + return Objects.requireNonNull(options.symbols.regularSymbols, + String.format("Type '%s' requires a regular alphabet definition", options.type)); + } + + private static ContextFreeSymbols validateContextFreeSymbols(Options options) { + return Objects.requireNonNull(options.symbols.contextFreeSymbols, + String.format("Type '%s' requires a context-free alphabet definition", + options.type)); + } +} diff --git a/cli/src/main/java/de/learnlib/cli/factory/EQOFactory.java b/cli/src/main/java/de/learnlib/cli/factory/EQOFactory.java new file mode 100644 index 000000000..ca79845f7 --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/factory/EQOFactory.java @@ -0,0 +1,264 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.factory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.regex.Pattern; + +import de.learnlib.cli.option.EQOracle; +import de.learnlib.cli.option.Options; +import de.learnlib.oracle.AdaptiveMembershipOracle; +import de.learnlib.oracle.EquivalenceOracle; +import de.learnlib.oracle.MembershipOracle; +import de.learnlib.oracle.equivalence.EQOracleChain; +import de.learnlib.oracle.equivalence.KWayStateCoverEQOracle; +import de.learnlib.oracle.equivalence.KWayTransitionCoverEQOracle; +import de.learnlib.oracle.equivalence.RandomWMethodEQOracle; +import de.learnlib.oracle.equivalence.RandomWordsEQOracle; +import de.learnlib.oracle.equivalence.RandomWpMethodEQOracle; +import de.learnlib.oracle.equivalence.SampleSetEQOracle; +import de.learnlib.oracle.equivalence.WMethodEQOracle; +import de.learnlib.oracle.equivalence.WpMethodEQOracle; +import de.learnlib.oracle.equivalence.vpa.RandomWellMatchedWordsEQOracle; +import de.learnlib.util.mealy.Adaptive2MembershipWrapper; +import net.automatalib.alphabet.impl.Alphabets; +import net.automatalib.automaton.UniversalDeterministicAutomaton.RegularAutomaton; +import net.automatalib.automaton.concept.SuffixOutput; +import net.automatalib.automaton.fsa.DFA; +import net.automatalib.automaton.fsa.NFA; +import net.automatalib.automaton.procedural.SBA; +import net.automatalib.automaton.procedural.SPA; +import net.automatalib.automaton.procedural.SPMM; +import net.automatalib.automaton.vpa.OneSEVPA; +import net.automatalib.util.automaton.fsa.NFAs; +import net.automatalib.word.Word; + +public final class EQOFactory { + + public static final int BATCH_SIZE = 10; + public static final double RANDOM_CALL_PROB = 0.5; + + private EQOFactory() { + // prevent instantiation + } + + public static & SuffixOutput, D> EQOracleChain getRegularOracles( + Options options, + MembershipOracle mqo) { + final EQOracleChain chain = new EQOracleChain<>(); + + for (EQOracle e : options.eqos) { + EquivalenceOracle eqo = switch (e) { + case W -> new WMethodEQOracle<>(mqo, + options.eqoParams.wMethod.lookahead, + options.eqoParams.wMethod.expectedSize, + computeBatchSize(options)); + case WP -> new WpMethodEQOracle<>(mqo, + options.eqoParams.wpMethod.lookahead, + options.eqoParams.wpMethod.expectedSize, + computeBatchSize(options)); + case RANDOM -> new RandomWordsEQOracle<>(mqo, + options.eqoParams.random.minLength, + options.eqoParams.random.maxLength, + options.eqoParams.random.maxTests, + new Random(options.eqoParams.random.seed), + computeBatchSize(options)); + case RANDOM_W -> new RandomWMethodEQOracle<>(mqo, + options.eqoParams.randomWMethod.minimalSize, + options.eqoParams.randomWMethod.rndLength, + options.eqoParams.randomWMethod.bound, + new Random(options.eqoParams.randomWMethod.seed), + computeBatchSize(options)); + case RANDOM_WP -> new RandomWpMethodEQOracle<>(mqo, + options.eqoParams.randomWpMethod.minimalSize, + options.eqoParams.randomWpMethod.rndLength, + options.eqoParams.randomWpMethod.bound, + new Random(options.eqoParams.randomWpMethod.seed), + computeBatchSize(options)); + case KWAY_S -> new KWayStateCoverEQOracle<>(mqo, + new Random(options.eqoParams.kWayState.seed), + options.eqoParams.kWayState.randomWalkLen, + options.eqoParams.kWayState.k, + options.eqoParams.kWayState.combinationMethod, + computeBatchSize(options)); + case KWAY_T -> new KWayTransitionCoverEQOracle<>(mqo, + new Random(options.eqoParams.kWayTransition.seed), + options.eqoParams.kWayTransition.randomWalkLen, + options.eqoParams.kWayTransition.numGeneratePaths, + options.eqoParams.kWayTransition.maxPathLen, + options.eqoParams.kWayTransition.maxNumberOfSteps, + options.eqoParams.kWayTransition.k, + options.eqoParams.kWayTransition.optimizationMetric, + options.eqoParams.kWayTransition.generationMethod, + computeBatchSize(options)); + case SAMPLE -> buildSampleSetOracle(options, mqo); + }; + chain.addOracle(eqo); + } + + return chain; + } + + public static & SuffixOutput>, O> EQOracleChain> getAdaptiveOracles( + Options options, + AdaptiveMembershipOracle mqo) { + return getRegularOracles(options, new Adaptive2MembershipWrapper<>(mqo)); + } + + public static EQOracleChain, String, Boolean> getNFAOracles(Options options, + MembershipOracle mqo) { + final EQOracleChain, String, Boolean> chain = getRegularOracles(options, mqo); + final EQOracleChain, String, Boolean> result = new EQOracleChain<>(); + + for (EquivalenceOracle, String, Boolean> eqo : chain.getOracles()) { + result.addOracle((hyp, inputs) -> eqo.findCounterExample(NFAs.determinize(hyp, + Alphabets.fromCollection(inputs)), + inputs)); + } + + return result; + } + + public static EQOracleChain, String, Boolean> getSBAOracles(Options options, + MembershipOracle mqo) { + final EQOracleChain, String, Boolean> chain = new EQOracleChain<>(); + + for (EQOracle e : options.eqos) { + EquivalenceOracle, String, Boolean> eqo = switch (e) { + case W -> new de.learnlib.oracle.equivalence.sba.WMethodEQOracle<>(mqo, + options.eqoParams.wMethod.lookahead, + options.eqoParams.wMethod.expectedSize, + computeBatchSize(options)); + case RANDOM -> buildRandomWellMatchedOracle(options, mqo); + case SAMPLE -> buildSampleSetOracle(options, mqo); + default -> throw new UnsupportedCombinationException(options, e); + }; + chain.addOracle(eqo); + } + + return chain; + } + + public static EQOracleChain, String, Boolean> getSPAOracles(Options options, + MembershipOracle mqo) { + final EQOracleChain, String, Boolean> chain = new EQOracleChain<>(); + + for (EQOracle e : options.eqos) { + EquivalenceOracle, String, Boolean> eqo = switch (e) { + case W -> new de.learnlib.oracle.equivalence.spa.WMethodEQOracle<>(mqo, + options.eqoParams.wMethod.lookahead, + options.eqoParams.wMethod.expectedSize, + computeBatchSize(options)); + case WP -> new de.learnlib.oracle.equivalence.spa.WpMethodEQOracle<>(mqo, + options.eqoParams.wpMethod.lookahead, + options.eqoParams.wpMethod.expectedSize, + computeBatchSize(options)); + case RANDOM -> buildRandomWellMatchedOracle(options, mqo); + case SAMPLE -> buildSampleSetOracle(options, mqo); + default -> throw new UnsupportedCombinationException(options, e); + }; + chain.addOracle(eqo); + } + + return chain; + } + + public static EQOracleChain, String, Word> getSPMMOracles(Options options, + MembershipOracle> mqo) { + final EQOracleChain, String, Word> chain = new EQOracleChain<>(); + + for (EQOracle e : options.eqos) { + EquivalenceOracle, String, Word> eqo = switch (e) { + case W -> new de.learnlib.oracle.equivalence.spmm.WMethodEQOracle<>(mqo, + options.eqoParams.wMethod.lookahead, + options.eqoParams.wMethod.expectedSize, + computeBatchSize(options)); + case RANDOM -> buildRandomWellMatchedOracle(options, mqo); + case SAMPLE -> buildSampleSetOracle(options, mqo); + default -> throw new UnsupportedCombinationException(options, e); + }; + chain.addOracle(eqo); + } + + return chain; + } + + public static EQOracleChain, String, Boolean> getVPAOracles(Options options, + MembershipOracle mqo) { + final EQOracleChain, String, Boolean> chain = new EQOracleChain<>(); + + for (EQOracle e : options.eqos) { + EquivalenceOracle, String, Boolean> eqo = switch (e) { + case RANDOM -> buildRandomWellMatchedOracle(options, mqo); + case SAMPLE -> buildSampleSetOracle(options, mqo); + default -> throw new UnsupportedCombinationException(options, e); + }; + chain.addOracle(eqo); + } + + return chain; + } + + private static RandomWellMatchedWordsEQOracle buildRandomWellMatchedOracle(Options options, + MembershipOracle oracle) { + return new RandomWellMatchedWordsEQOracle<>(new Random(options.eqoParams.random.seed), + oracle, + RANDOM_CALL_PROB, + options.eqoParams.random.maxTests, + options.eqoParams.random.minLength, + options.eqoParams.random.maxLength, + computeBatchSize(options)); + } + + private static SampleSetEQOracle buildSampleSetOracle(Options options, + MembershipOracle oracle) { + final List samples = options.eqoParams.samples.samples; + final SampleSetEQOracle sampleSetOracle = new SampleSetEQOracle<>(); + + if (samples != null) { + + final Pattern pattern = Pattern.compile(options.eqoParams.samples.split); + final List> tmp = new ArrayList<>(samples.size()); + + for (String s : samples) { + String[] words = pattern.split(s); + tmp.add(Word.fromArray(words, 0, words.length)); + } + + sampleSetOracle.addAll(oracle, tmp); + } + + return sampleSetOracle; + } + + private static int computeBatchSize(Options options) { + if (options.sul.size() == 1) { + return 1; + } else { + return options.sul.size() * BATCH_SIZE; + } + } + + private static final class UnsupportedCombinationException extends IllegalArgumentException { + + UnsupportedCombinationException(Options options, EQOracle eqo) { + super(String.format("Type '%s' does not support oracle '%s'", options.type, eqo)); + } + } + +} diff --git a/cli/src/main/java/de/learnlib/cli/factory/LearnerFactory.java b/cli/src/main/java/de/learnlib/cli/factory/LearnerFactory.java new file mode 100644 index 000000000..2dbc10368 --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/factory/LearnerFactory.java @@ -0,0 +1,145 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.factory; + +import de.learnlib.algorithm.adt.learner.ADTLearner; +import de.learnlib.algorithm.lsharp.LSharpMealy; +import de.learnlib.algorithm.nlstar.NLStarLearner; +import de.learnlib.algorithm.observationpack.vpa.OPLearnerVPABuilder; +import de.learnlib.algorithm.procedural.SymbolWrapper; +import de.learnlib.algorithm.procedural.sba.SBALearner; +import de.learnlib.algorithm.procedural.spa.SPALearner; +import de.learnlib.algorithm.procedural.spmm.SPMMLearner; +import de.learnlib.algorithm.ttt.vpa.TTTLearnerVPABuilder; +import de.learnlib.cli.adapter.ProceduralDFAAdapter.ExtensibleLStarDFAAdapter; +import de.learnlib.cli.adapter.ProceduralDFAAdapter.KearnsVaziraniDFAAdapter; +import de.learnlib.cli.adapter.ProceduralDFAAdapter.LLambdaDFAAdapter; +import de.learnlib.cli.adapter.ProceduralDFAAdapter.MalerPnueliDFAAdapter; +import de.learnlib.cli.adapter.ProceduralDFAAdapter.OPLearnerDFAAdapter; +import de.learnlib.cli.adapter.ProceduralDFAAdapter.RivestSchapireDFAAdapter; +import de.learnlib.cli.adapter.ProceduralDFAAdapter.TTTLambdaDFAAdapter; +import de.learnlib.cli.adapter.ProceduralDFAAdapter.TTTLearnerDFAAdapter; +import de.learnlib.cli.adapter.ProceduralMealyAdapter.ExtensibleLStarMealyAdapter; +import de.learnlib.cli.adapter.ProceduralMealyAdapter.KearnsVaziraniMealyAdapter; +import de.learnlib.cli.adapter.ProceduralMealyAdapter.LLambdaMealyAdapter; +import de.learnlib.cli.adapter.ProceduralMealyAdapter.MalerPnueliMealyAdapter; +import de.learnlib.cli.adapter.ProceduralMealyAdapter.MealyDHCAdapter; +import de.learnlib.cli.adapter.ProceduralMealyAdapter.OPLearnerMealyAdapter; +import de.learnlib.cli.adapter.ProceduralMealyAdapter.RivestSchapireMealyAdapter; +import de.learnlib.cli.adapter.ProceduralMealyAdapter.SparseLearnerAdapter; +import de.learnlib.cli.adapter.ProceduralMealyAdapter.TTTLambdaMealyAdapter; +import de.learnlib.cli.adapter.ProceduralMealyAdapter.TTTLearnerMealyAdapter; +import de.learnlib.cli.option.Learner; +import de.learnlib.cli.option.Options; +import de.learnlib.cli.util.Constructor.AdaptiveConstructor; +import de.learnlib.cli.util.Constructor.DFAConstructor; +import de.learnlib.cli.util.Constructor.MealyConstructor; +import de.learnlib.cli.util.Constructor.PresetConstructor; +import net.automatalib.alphabet.Alphabet; +import net.automatalib.alphabet.ProceduralInputAlphabet; +import net.automatalib.alphabet.VPAlphabet; +import net.automatalib.automaton.fsa.NFA; +import net.automatalib.automaton.procedural.SBA; +import net.automatalib.automaton.procedural.SPA; +import net.automatalib.automaton.procedural.SPMM; +import net.automatalib.automaton.transducer.MealyMachine; +import net.automatalib.automaton.vpa.OneSEVPA; +import net.automatalib.word.Word; + +public final class LearnerFactory { + + private LearnerFactory() { + // prevent instantiation + } + + public static DFAConstructor, I> getDFALearner(Options options) { + return switch (options.learner) { + case KEARNS_VAZIRANI -> KearnsVaziraniDFAAdapter::new; + case L_LAMBDA -> LLambdaDFAAdapter::new; + case L_STAR -> ExtensibleLStarDFAAdapter::new; + case MALER_PNUELI -> MalerPnueliDFAAdapter::new; + case OBSERVATION_PACK -> OPLearnerDFAAdapter::new; + case RIVEST_SCHAPIRE -> RivestSchapireDFAAdapter::new; + case TTT -> TTTLearnerDFAAdapter::new; + case TTT_LAMBDA -> TTTLambdaDFAAdapter::new; + default -> throw new UnsupportedCombinationException(options); + }; + } + + public static MealyConstructor, I, O> getMealyLearner(Options options) { + return switch (options.learner) { + case DHC -> MealyDHCAdapter::new; + case KEARNS_VAZIRANI -> KearnsVaziraniMealyAdapter::new; + case L_LAMBDA -> LLambdaMealyAdapter::new; + case L_STAR -> ExtensibleLStarMealyAdapter::new; + case MALER_PNUELI -> MalerPnueliMealyAdapter::new; + case OBSERVATION_PACK -> OPLearnerMealyAdapter::new; + case RIVEST_SCHAPIRE -> RivestSchapireMealyAdapter::new; + case SPARSE -> SparseLearnerAdapter::new; + case TTT -> TTTLearnerMealyAdapter::new; + case TTT_LAMBDA -> TTTLambdaMealyAdapter::new; + default -> throw new UnsupportedCombinationException(options); + }; + } + + public static AdaptiveConstructor, MealyMachine, I, O> getAdaptiveLearner(Options options) { + return switch (options.learner) { + case ADT -> ADTLearner::new; + case L_SHARP -> LSharpMealy::new; + default -> throw new UnsupportedCombinationException(options); + }; + } + + public static PresetConstructor, NFA, I, Boolean> getNFALearner(Options options) { + if (options.learner == Learner.NL_STAR) { + return NLStarLearner::new; + } + throw new UnsupportedCombinationException(options); + } + + public static PresetConstructor, SBA, I, Boolean> getSBALearner(Options options) { + final DFAConstructor>, SymbolWrapper> learner = getDFALearner(options); + return (alph, mqo) -> new SBALearner<>(alph, mqo, learner::constructLearner); + } + + public static PresetConstructor, SPA, I, Boolean> getSPALearner(Options options) { + final DFAConstructor, I> learner = getDFALearner(options); + return (alph, mqo) -> new SPALearner<>(alph, mqo, learner::constructLearner); + } + + public static PresetConstructor, SPMM, I, Word> getSPMMLearner( + Options options) { + final MealyConstructor>, SymbolWrapper, String> learner = getMealyLearner(options); + return (alph, mqo) -> new SPMMLearner<>(alph, "error", mqo, learner::constructLearner); + } + + public static PresetConstructor, OneSEVPA, I, Boolean> getVPALearner(Options options) { + return switch (options.learner) { + case OBSERVATION_PACK -> + (alphabet, mqo) -> new OPLearnerVPABuilder().withAlphabet(alphabet).withOracle(mqo).create(); + case TTT -> + (alphabet, mqo) -> new TTTLearnerVPABuilder().withAlphabet(alphabet).withOracle(mqo).create(); + default -> throw new UnsupportedCombinationException(options); + }; + } + + private static final class UnsupportedCombinationException extends IllegalArgumentException { + + UnsupportedCombinationException(Options options) { + super(String.format("Learner '%s' does not support type '%s'", options.learner, options.type)); + } + } +} diff --git a/cli/src/main/java/de/learnlib/cli/factory/MQOFactory.java b/cli/src/main/java/de/learnlib/cli/factory/MQOFactory.java new file mode 100644 index 000000000..dfe4395de --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/factory/MQOFactory.java @@ -0,0 +1,223 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.factory; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; +import java.util.regex.Pattern; + +import de.learnlib.cli.option.Options; +import de.learnlib.filter.cache.dfa.DFACaches; +import de.learnlib.filter.cache.mealy.MealyCaches; +import de.learnlib.filter.statistic.oracle.CounterAdaptiveQueryOracle; +import de.learnlib.filter.statistic.oracle.CounterOracle; +import de.learnlib.oracle.AdaptiveMembershipOracle; +import de.learnlib.oracle.MembershipOracle; +import de.learnlib.oracle.membership.CLIOracle; +import de.learnlib.oracle.membership.CLIOutputAdaptiveOracle; +import de.learnlib.oracle.membership.CLIOutputOracle; +import de.learnlib.oracle.membership.StdInOracle; +import de.learnlib.oracle.membership.StdInOutputAdaptiveOracle; +import de.learnlib.oracle.membership.StdInOutputOracle; +import de.learnlib.oracle.parallelism.ParallelOracleBuilders; +import net.automatalib.alphabet.Alphabet; +import net.automatalib.word.Word; + +public final class MQOFactory { + + static final String SUL_KEY = "sul"; + static final String CACHE_KEY = "cache"; + + private MQOFactory() { + // prevent instantiation + } + + public static MembershipOracle getAcceptorOracle(Options options, Alphabet alphabet) { + MembershipOracle oracle; + + if (options.sul.size() == 1) { + oracle = buildSingleAcceptorOracle(options, options.sul.get(0)); + } else { + final List> suls = new ArrayList<>(options.sul.size()); + for (File sul : options.sul) { + suls.add(buildSingleAcceptorOracle(options, sul)); + } + oracle = ParallelOracleBuilders.newStaticParallelOracle(suls).create(); + } + + if (options.statistics) { + oracle = new CounterOracle<>(oracle, SUL_KEY); + } + + if (options.cache) { + oracle = DFACaches.createDAGCache(alphabet, oracle); + if (options.statistics) { + oracle = new CounterOracle<>(oracle, CACHE_KEY); + } + } + + return oracle; + } + + static MembershipOracle buildSingleAcceptorOracle(Options options, File path) { + if (options.stdin) { + return new StdInOracle<>(buildCommandLine(options, path), options.reset); + } else { + return new CLIOracle<>(buildCommandLine(options, path), options.reset); + } + } + + public static MembershipOracle> getTransducerOracle(Options options, + Alphabet alphabet) { + MembershipOracle> oracle; + + if (options.sul.size() == 1) { + oracle = buildSingleTransducerOracle(options, options.sul.get(0)); + } else { + final List>> suls = new ArrayList<>(options.sul.size()); + for (File sul : options.sul) { + suls.add(buildSingleTransducerOracle(options, sul)); + } + oracle = ParallelOracleBuilders.newStaticParallelOracle(suls).create(); + } + + if (options.statistics) { + oracle = new CounterOracle<>(oracle, SUL_KEY); + } + + if (options.cache) { + oracle = MealyCaches.createDAGCache(alphabet, oracle); + if (options.statistics) { + oracle = new CounterOracle<>(oracle, CACHE_KEY); + } + } + + return oracle; + } + + static MembershipOracle> buildSingleTransducerOracle(Options options, File path) { + if (options.stdin) { + return new StdInOutputOracle<>(buildCommandLine(options, path), + new OutputTransformer(options), + options.reset); + } else { + return new CLIOutputOracle<>(buildCommandLine(options, path), + new OutputTransformer(options), + options.reset); + } + } + + public static AdaptiveMembershipOracle getAdaptiveOracle(Options options, + Alphabet alphabet) { + verifyReset(options); + + AdaptiveMembershipOracle oracle; + + if (options.sul.size() == 1) { + oracle = buildSingleAdaptiveOracle(options, options.sul.get(0)); + } else { + final List> suls = new ArrayList<>(options.sul.size()); + for (File sul : options.sul) { + suls.add(buildSingleAdaptiveOracle(options, sul)); + } + oracle = ParallelOracleBuilders.newStaticParallelAdaptiveOracle(suls).create(); + } + + if (options.statistics) { + oracle = new CounterAdaptiveQueryOracle<>(oracle, SUL_KEY); + } + + if (options.cache) { + oracle = MealyCaches.createAdaptiveQueryCache(alphabet, oracle); + if (options.statistics) { + oracle = new CounterAdaptiveQueryOracle<>(oracle, CACHE_KEY); + } + } + + return oracle; + } + + static AdaptiveMembershipOracle buildSingleAdaptiveOracle(Options options, File path) { + if (options.stdin) { + return new StdInOutputAdaptiveOracle<>(buildCommandLine(options, path), Function.identity(), options.reset); + } else { + return new CLIOutputAdaptiveOracle<>(buildCommandLine(options, path), Function.identity(), options.reset); + } + } + + private static List buildCommandLine(Options options, File file) { + verifyPath(file); + final String absolutePath = file.getAbsolutePath(); + if (options.additionalArgs == null) { + return Collections.singletonList(absolutePath); + } else { + final List cmd = new ArrayList<>(options.additionalArgs.size() + 1); + cmd.add(absolutePath); + cmd.addAll(options.additionalArgs); + return cmd; + } + } + + private static void verifyPath(File file) { + if (!file.exists()) { + throw new IllegalArgumentException(String.format("Specified SUL '%s' does not exist", file)); + } + } + + private static void verifyReset(Options options) { + if (options.reset == null) { + throw new IllegalArgumentException(String.format( + "Learner '%s' requires a stateful oracle. Provide a --reset", + options.learner)); + } + } + + private static class OutputTransformer implements CLIOutputOracle.OutputTransformer>, + StdInOutputOracle.OutputTransformer> { + + private final Pattern pattern; + + OutputTransformer(Options options) { + this.pattern = Pattern.compile(options.delimiter); + } + + @Override + public Word transform(String output, int prefixLength, int suffixLength) { + if (suffixLength == 0) { + return Word.epsilon(); + } + + if (output.isBlank()) { + throw new IllegalStateException("Received empty output when non-empty output was expected."); + } + + final String[] orig = pattern.split(output); + + if (orig.length != (prefixLength + suffixLength)) { + throw new IllegalStateException(String.format( + "The parsed output '%s' does not have the expected number (%d) of symbols.", + Arrays.toString(orig), + prefixLength + suffixLength)); + } + + return Word.fromArray(orig, prefixLength, suffixLength); + } + } +} diff --git a/cli/src/main/java/de/learnlib/cli/factory/SerializerFactory.java b/cli/src/main/java/de/learnlib/cli/factory/SerializerFactory.java new file mode 100644 index 000000000..3a0dcac1e --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/factory/SerializerFactory.java @@ -0,0 +1,122 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.factory; + +import java.io.DataOutput; + +import de.learnlib.cli.option.Options; +import de.learnlib.cli.option.Output; +import net.automatalib.automaton.fsa.DFA; +import net.automatalib.automaton.fsa.NFA; +import net.automatalib.automaton.procedural.SBA; +import net.automatalib.automaton.procedural.SPA; +import net.automatalib.automaton.procedural.SPMM; +import net.automatalib.automaton.transducer.MealyMachine; +import net.automatalib.automaton.vpa.OneSEVPA; +import net.automatalib.serialization.InputModelSerializer; +import net.automatalib.serialization.aut.AUTWriter; +import net.automatalib.serialization.ba.BAWriter; +import net.automatalib.serialization.dot.DOTSerializationProvider; +import net.automatalib.serialization.learnlibv2.LearnLibV2Serialization; +import net.automatalib.serialization.mata.writer.MataNFAWriter; +import net.automatalib.serialization.saf.SAFWriters; +import net.automatalib.serialization.taf.writer.TAFWriters; + +public final class SerializerFactory { + + private SerializerFactory() { + // prevent instantiation + } + + public static InputModelSerializer> getDFASerializer(Options options) { + final InputModelSerializer> dfaSerializer = + SerializerFactory.getDFASerializerInternal(options); + return dfaSerializer::writeModel; + } + + private static InputModelSerializer> getDFASerializerInternal(Options options) { + return switch (options.format) { + case AUT -> new AUTWriter<>(); + case BA -> new BAWriter<>(); + case DOT -> DOTSerializationProvider.forAutomaton(); + case LEARNLIBV2 -> LearnLibV2Serialization.getInstance(); + case MATA -> new MataNFAWriter<>(); + case SAF -> SAFWriters.dfa(); + case TAF -> TAFWriters.dfa(); + }; + } + + public static InputModelSerializer> getMealySerializer(Options options) { + return switch (options.format) { + case DOT -> DOTSerializationProvider.forAutomaton(); + case SAF -> SAFWriters.mealy(DataOutput::writeUTF); + case TAF -> TAFWriters.mealy(); + default -> throw new UnsupportedCombinationException(options); + }; + } + + public static InputModelSerializer> getNFASerializer(Options options) { + final InputModelSerializer> nfaSerializer = + SerializerFactory.getNFASerializerInternal(options); + return nfaSerializer::writeModel; + } + + private static InputModelSerializer> getNFASerializerInternal(Options options) { + return switch (options.format) { + case AUT -> new AUTWriter<>(); + case BA -> new BAWriter<>(); + case DOT -> DOTSerializationProvider.forAutomaton(); + case MATA -> new MataNFAWriter<>(); + case SAF -> SAFWriters.nfa(); + default -> throw new UnsupportedCombinationException(options); + }; + } + + public static InputModelSerializer> getSBASerializer(Options options) { + if (options.format == Output.DOT) { + return DOTSerializationProvider.forGraphViewableInput(); + } + throw new UnsupportedCombinationException(options); + } + + public static InputModelSerializer> getSPASerializer(Options options) { + if (options.format == Output.DOT) { + return DOTSerializationProvider.forGraphViewableInput(); + } + throw new UnsupportedCombinationException(options); + } + + public static InputModelSerializer> getSPMMSerializer(Options options) { + if (options.format == Output.DOT) { + return DOTSerializationProvider.forGraphViewableInput(); + } + throw new UnsupportedCombinationException(options); + } + + public static InputModelSerializer> getVPASerializer(Options options) { + if (options.format == Output.DOT) { + return DOTSerializationProvider.forGraphViewableInput(); + } + throw new UnsupportedCombinationException(options); + } + + private static final class UnsupportedCombinationException extends IllegalArgumentException { + + UnsupportedCombinationException(Options options) { + super(String.format("Type '%s' cannot be written into '%s' format", options.type, options.format)); + } + } +} diff --git a/cli/src/main/java/de/learnlib/cli/option/EQOParams.java b/cli/src/main/java/de/learnlib/cli/option/EQOParams.java new file mode 100644 index 000000000..f837e2db0 --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/option/EQOParams.java @@ -0,0 +1,246 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.option; + +import java.util.List; + +import net.automatalib.util.automaton.conformance.KWayStateCoverTestsIterator.CombinationMethod; +import net.automatalib.util.automaton.conformance.KWayTransitionCoverTestsIterator.GenerationMethod; +import net.automatalib.util.automaton.conformance.KWayTransitionCoverTestsIterator.OptimizationMetric; +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Option; + +public class EQOParams { + + @ArgGroup(validate = false) + public KWayStateMethod kWayState = new KWayStateMethod(); + @ArgGroup(validate = false) + public KWayTransitionMethod kWayTransition = new KWayTransitionMethod(); + @ArgGroup(validate = false) + public RandomMethod random = new RandomMethod(); + @ArgGroup(validate = false) + public RandomWMethod randomWMethod = new RandomWMethod(); + @ArgGroup(validate = false) + public RandomWpMethod randomWpMethod = new RandomWpMethod(); + @ArgGroup(validate = false) + public Samples samples = new Samples(); + @ArgGroup(validate = false) + public WMethod wMethod = new WMethod(); + @ArgGroup(validate = false) + public WpMethod wpMethod = new WpMethod(); + + public static class KWayStateMethod { + + @Option(names = "--eqo-kway-s-k", + defaultValue = "2", + paramLabel = "", + descriptionKey = "param.eqo.kway-s.k") + public int k; + + @Option(names = "--eqo-kway-s-randomWalkLen", + defaultValue = "20", + paramLabel = "", + descriptionKey = "param.eqo.kway-s.randomWalkLen") + public int randomWalkLen; + + @Option(names = "--eqo-kway-s-combinationMethod", + defaultValue = "PERMUTATIONS", + paramLabel = "", + descriptionKey = "param.eqo.kway-s.combinationMethod") + public CombinationMethod combinationMethod; + + @Option(names = "--eqo-kway-s-seed", + defaultValue = "42", + paramLabel = "", + descriptionKey = "param.eqo.kway-s.seed") + public int seed; + } + + public static class KWayTransitionMethod { + + @Option(names = "--eqo-kway-t-k", + defaultValue = "2", + paramLabel = "", + descriptionKey = "param.eqo.kway-t.k") + public int k; + + @Option(names = "--eqo-kway-t-randomWalkLen", + defaultValue = "10", + paramLabel = "", + descriptionKey = "param.eqo.kway-t.randomWalkLen") + public int randomWalkLen; + + @Option(names = "--eqo-kway-t-numGeneratePaths", + defaultValue = "100", + paramLabel = "", + descriptionKey = "param.eqo.kway-t.numGeneratePaths") + public int numGeneratePaths; + + @Option(names = "--eqo-kway-t-maxPathLen", + defaultValue = "50", + paramLabel = "", + descriptionKey = "param.eqo.kway-t.maxPathLen") + public int maxPathLen; + + @Option(names = "--eqo-kway-t-maxNumberOfSteps", + defaultValue = "100", + paramLabel = "", + descriptionKey = "param.eqo.kway-t.maxNumberOfSteps") + public int maxNumberOfSteps; + + @Option(names = "--eqo-kway-t-optimizationMetric", + defaultValue = "STEPS", + paramLabel = "", + descriptionKey = "param.eqo.kway-t.optimizationMetric") + public OptimizationMetric optimizationMetric; + + @Option(names = "--eqo-kway-t-generationMethod", + defaultValue = "RANDOM", + paramLabel = "", + descriptionKey = "param.eqo.kway-t.generationMethod") + public GenerationMethod generationMethod; + + @Option(names = "--eqo-kway-t-seed", + defaultValue = "42", + paramLabel = "", + descriptionKey = "param.eqo.kway-t.seed") + public int seed; + } + + public static class RandomMethod { + + @Option(names = "--eqo-random-minLength", + defaultValue = "10", + paramLabel = "", + descriptionKey = "param.eqo.random.minLength") + public int minLength; + + @Option(names = "--eqo-random-maxLength", + defaultValue = "20", + paramLabel = "", + descriptionKey = "param.eqo.random.maxLength") + public int maxLength; + + @Option(names = "--eqo-random-maxTests", + defaultValue = "100", + paramLabel = "", + descriptionKey = "param.eqo.random.maxTests") + public int maxTests; + + @Option(names = "--eqo-random-seed", + defaultValue = "42", + paramLabel = "", + descriptionKey = "param.eqo.random.seed") + public int seed; + } + + public static class RandomWMethod { + + @Option(names = "--eqo-random-w-minimalSize", + defaultValue = "0", + paramLabel = "", + descriptionKey = "param.eqo.random-w.minimalSize") + public int minimalSize; + + @Option(names = "--eqo-random-w-rndLength", + defaultValue = "5", + paramLabel = "", + descriptionKey = "param.eqo.random-w.rndLength") + public int rndLength; + + @Option(names = "--eqo-random-w-bound", + defaultValue = "100", + paramLabel = "", + descriptionKey = "param.eqo.random-w.bound") + public int bound; + + @Option(names = "--eqo-random-w-seed", + defaultValue = "42", + paramLabel = "", + descriptionKey = "param.eqo.random-w.seed") + public int seed; + } + + public static class RandomWpMethod { + + @Option(names = "--eqo-random-wp-minimalSize", + defaultValue = "0", + paramLabel = "", + descriptionKey = "param.eqo.random-wp.minimalSize") + public int minimalSize; + + @Option(names = "--eqo-random-wp-rndLength", + defaultValue = "5", + paramLabel = "", + descriptionKey = "param.eqo.random-wp.rndLength") + public int rndLength; + + @Option(names = "--eqo-random-wp-bound", + defaultValue = "100", + paramLabel = "", + descriptionKey = "param.eqo.random-wp.bound") + public int bound; + + @Option(names = "--eqo-random-wp-seed", + defaultValue = "42", + paramLabel = "", + descriptionKey = "param.eqo.random-wp.seed") + public int seed; + } + + public static class Samples { + + @Option(names = "--eqo-sample", paramLabel = "", descriptionKey = "param.eqo.sample") + public List samples; + + @Option(names = "--eqo-sample-split", + defaultValue = "\\s", + paramLabel = "", + descriptionKey = "param.eqo.sample.split") + public String split; + } + + public static class WMethod { + + @Option(names = "--eqo-w-lookahead", + defaultValue = "2", + paramLabel = "", + descriptionKey = "param.eqo.w.lookahead") + public int lookahead; + + @Option(names = "--eqo-w-expectedSize", + defaultValue = "5", + paramLabel = "", + descriptionKey = "param.eqo.w.expectedSize") + public int expectedSize; + } + + public static class WpMethod { + + @Option(names = "--eqo-wp-lookahead", + defaultValue = "2", + paramLabel = "", + descriptionKey = "param.eqo.wp.lookahead") + public int lookahead; + + @Option(names = "--eqo-wp-expectedSize", + defaultValue = "5", + paramLabel = "", + descriptionKey = "param.eqo.wp.expectedSize") + public int expectedSize; + } + +} diff --git a/cli/src/main/java/de/learnlib/cli/option/EQOracle.java b/cli/src/main/java/de/learnlib/cli/option/EQOracle.java new file mode 100644 index 000000000..c48f1946d --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/option/EQOracle.java @@ -0,0 +1,29 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.option; + +public enum EQOracle { + + KWAY_S, + KWAY_T, + RANDOM, + RANDOM_W, + RANDOM_WP, + SAMPLE, + W, + WP + +} diff --git a/cli/src/main/java/de/learnlib/cli/option/Learner.java b/cli/src/main/java/de/learnlib/cli/option/Learner.java new file mode 100644 index 000000000..da5e5d109 --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/option/Learner.java @@ -0,0 +1,33 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.option; + +public enum Learner { + + ADT, + DHC, + KEARNS_VAZIRANI, + L_LAMBDA, + L_SHARP, + L_STAR, + NL_STAR, + MALER_PNUELI, + OBSERVATION_PACK, + RIVEST_SCHAPIRE, + SPARSE, + TTT, + TTT_LAMBDA, +} diff --git a/cli/src/main/java/de/learnlib/cli/option/Options.java b/cli/src/main/java/de/learnlib/cli/option/Options.java new file mode 100644 index 000000000..7b3cc2932 --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/option/Options.java @@ -0,0 +1,79 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.option; + +import java.io.File; +import java.nio.file.Path; +import java.util.List; + +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +@SuppressWarnings("PMD.TooManyFields") +public class Options { + + @Parameters(paramLabel = "", descriptionKey = "option.sul", arity = "1..*") + public List sul; + + @Option(names = {"-t", "--type"}, defaultValue = "DFA", descriptionKey = "option.type") + public Type type; + + @Option(names = {"-l", "--learner"}, defaultValue = "TTT", descriptionKey = "option.learner") + public Learner learner; + + @Option(names = {"-r", "--reset"}, paramLabel = "", descriptionKey = "option.reset") + public String reset; + + @Option(names = {"-d", "--delim"}, paramLabel = "", defaultValue = "\\n", descriptionKey = "option.delim") + public String delimiter; + + @Option(names = {"-e", "--eqo"}, paramLabel = "", defaultValue = "RANDOM_WP", descriptionKey = "option.eqo") + public List eqos; + + @Option(names = {"-c", "--cache"}, descriptionKey = "option.cache") + public boolean cache; + + @Option(names = "--stats", descriptionKey = "option.stats") + public boolean statistics; + + @Option(names = "--stdin", descriptionKey = "option.stdin") + public boolean stdin; + + @Option(names = "--args", paramLabel = "", descriptionKey = "option.args") + public List additionalArgs; + + @Option(names = {"-f", "--format"}, defaultValue = "DOT", descriptionKey = "option.format") + public Output format; + + @Option(names = {"-o", "--output"}, paramLabel = "", descriptionKey = "option.output") + public Path output; + + @Option(names = "--resume-from", paramLabel = "", descriptionKey = "option.resume") + public Path resumeFrom; + + @Option(names = "--snapshot-dir", paramLabel = "", descriptionKey = "option.snapshot") + public Path snapshotDir; + + @Option(names = {"-v", "--verbose"}, descriptionKey = "option.verbose") + public boolean[] verbosity; + + @ArgGroup(multiplicity = "1") + public Symbols symbols; + + @ArgGroup(validate = false, headingKey = "param.eqo.heading") + public EQOParams eqoParams = new EQOParams(); +} diff --git a/cli/src/main/java/de/learnlib/cli/option/Output.java b/cli/src/main/java/de/learnlib/cli/option/Output.java new file mode 100644 index 000000000..3844418d7 --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/option/Output.java @@ -0,0 +1,27 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.option; + +public enum Output { + + AUT, + BA, + DOT, + LEARNLIBV2, + MATA, + TAF, + SAF, +} diff --git a/cli/src/main/java/de/learnlib/cli/option/Symbols.java b/cli/src/main/java/de/learnlib/cli/option/Symbols.java new file mode 100644 index 000000000..7c27dc45e --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/option/Symbols.java @@ -0,0 +1,59 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.option; + +import java.util.List; + +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Option; + +public class Symbols { + + @ArgGroup(headingKey = "param.symbol.reg.heading") + public RegularSymbols regularSymbols; + + @ArgGroup(exclusive = false, headingKey = "param.symbol.cf.heading") + public ContextFreeSymbols contextFreeSymbols; + + public static class RegularSymbols { + + @Option(names = {"-s", "--symbol"}, + required = true, + paramLabel = "", + descriptionKey = "param.symbol.reg.description") + public List symbols; + } + + public static class ContextFreeSymbols { + + @Option(names = "--call", + required = true, + paramLabel = "", + descriptionKey = "param.symbol.cf.call.description") + public List callSymbols; + @Option(names = "--int", + required = true, + paramLabel = "", + descriptionKey = "param.symbol.cf.int.description") + public List internalSymbols; + @Option(names = "--ret", + required = true, + paramLabel = "", + descriptionKey = "param.symbol.cf.ret.description") + public List returnSymbols; + } + +} diff --git a/cli/src/main/java/de/learnlib/cli/option/Type.java b/cli/src/main/java/de/learnlib/cli/option/Type.java new file mode 100644 index 000000000..a8e0b9cf2 --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/option/Type.java @@ -0,0 +1,109 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.option; + +import de.learnlib.cli.factory.AlphabetFactory; +import de.learnlib.cli.factory.EQOFactory; +import de.learnlib.cli.factory.LearnerFactory; +import de.learnlib.cli.factory.MQOFactory; +import de.learnlib.cli.factory.SerializerFactory; +import de.learnlib.cli.util.AdaptiveRunner; +import de.learnlib.cli.util.PresetRunner; +import de.learnlib.cli.util.Runner; + +public enum Type { + DFA { + @Override + public Runner runner(Options options) { + return new PresetRunner<>(AlphabetFactory::getRegularAlphabet, + MQOFactory::getAcceptorOracle, + LearnerFactory::getDFALearner, + EQOFactory::getRegularOracles, + SerializerFactory::getDFASerializer); + } + }, + MEALY { + @Override + public Runner runner(Options options) { + if (options.learner == Learner.ADT || options.learner == Learner.L_SHARP) { + return new AdaptiveRunner<>(AlphabetFactory::getRegularAlphabet, + MQOFactory::getAdaptiveOracle, + LearnerFactory::getAdaptiveLearner, + EQOFactory::getAdaptiveOracles, + SerializerFactory::getMealySerializer); + } else { + return new PresetRunner<>(AlphabetFactory::getRegularAlphabet, + MQOFactory::getTransducerOracle, + LearnerFactory::getMealyLearner, + EQOFactory::getRegularOracles, + SerializerFactory::getMealySerializer); + } + } + }, + NFA { + @Override + public Runner runner(Options options) { + return new PresetRunner<>(AlphabetFactory::getRegularAlphabet, + MQOFactory::getAcceptorOracle, + LearnerFactory::getNFALearner, + EQOFactory::getNFAOracles, + SerializerFactory::getNFASerializer); + } + }, + SBA { + @Override + public Runner runner(Options options) { + return new PresetRunner<>(AlphabetFactory::getProceduralAlphabet, + MQOFactory::getAcceptorOracle, + LearnerFactory::getSBALearner, + EQOFactory::getSBAOracles, + SerializerFactory::getSBASerializer); + } + }, + SPA { + @Override + public Runner runner(Options options) { + return new PresetRunner<>(AlphabetFactory::getProceduralAlphabet, + MQOFactory::getAcceptorOracle, + LearnerFactory::getSPALearner, + EQOFactory::getSPAOracles, + SerializerFactory::getSPASerializer); + } + }, + SPMM { + @Override + public Runner runner(Options options) { + return new PresetRunner<>(AlphabetFactory::getProceduralAlphabet, + MQOFactory::getTransducerOracle, + LearnerFactory::getSPMMLearner, + EQOFactory::getSPMMOracles, + SerializerFactory::getSPMMSerializer); + } + }, + VPA { + @Override + public Runner runner(Options options) { + return new PresetRunner<>(AlphabetFactory::getVPAlphabet, + MQOFactory::getAcceptorOracle, + LearnerFactory::getVPALearner, + EQOFactory::getVPAOracles, + SerializerFactory::getVPASerializer); + } + }; + + public abstract Runner runner(Options options); + +} diff --git a/cli/src/main/java/de/learnlib/cli/util/AbstractRunner.java b/cli/src/main/java/de/learnlib/cli/util/AbstractRunner.java new file mode 100644 index 000000000..0891621b8 --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/util/AbstractRunner.java @@ -0,0 +1,132 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.function.BiFunction; +import java.util.function.Function; + +import de.learnlib.Resumable; +import de.learnlib.algorithm.LearningAlgorithm; +import de.learnlib.cli.option.Options; +import de.learnlib.logging.Category; +import de.learnlib.oracle.EquivalenceOracle; +import de.learnlib.statistic.Statistics; +import de.learnlib.statistic.StatisticsService; +import de.learnlib.util.Experiment; +import net.automatalib.alphabet.Alphabet; +import net.automatalib.automaton.concept.FiniteRepresentation; +import net.automatalib.serialization.InputModelSerializer; +import net.automatalib.ts.simple.SimpleTS; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class AbstractRunner, M extends SimpleTS & FiniteRepresentation, I, D, OR> + implements Runner { + + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractRunner.class); + + public static final String LEARNER_KEY = "learner"; + public static final String EQO_KEY = "eqo"; + + private final Function alphabetCreator; + private final BiFunction mqoCreator; + private final Function> learnerCreator; + private final BiFunction> eqoCreator; + private final Function> serializerCreator; + + public AbstractRunner(Function alphabetCreator, + BiFunction mqoCreator, + Function> learnerCreator, + BiFunction> eqoCreator, + Function> serializerCreator) { + this.alphabetCreator = alphabetCreator; + this.mqoCreator = mqoCreator; + this.learnerCreator = learnerCreator; + this.eqoCreator = eqoCreator; + this.serializerCreator = serializerCreator; + } + + @Override + public void run(Options options) { + + final A alphabet = alphabetCreator.apply(options); + final OR mqo = mqoCreator.apply(options, alphabet); + + final OR learnerOracle; + if (options.statistics) { + learnerOracle = getCounter(mqo, LEARNER_KEY); + } else { + learnerOracle = mqo; + } + + final LearningAlgorithm learner = + learnerCreator.apply(options).constructLearner(alphabet, learnerOracle); + + final OR eqoOracle; + if (options.statistics) { + eqoOracle = getCounter(mqo, EQO_KEY); + } else { + eqoOracle = mqo; + } + + final EquivalenceOracle eqo = eqoCreator.apply(options, eqoOracle); + final InputModelSerializer serializer = serializerCreator.apply(options); + final Experiment experiment = buildExperiment(learner, eqo, alphabet, serializer, options); + + final M hyp = experiment.run(); + + if (options.statistics) { + final StatisticsService service = Statistics.getService(); + LOGGER.info(Category.STATISTIC, service.print()); + } + + try { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + serializer.writeModel(baos, hyp, alphabet); + + LOGGER.info(Category.MODEL, "Final hypothesis:\n{}", baos.toString(StandardCharsets.UTF_8)); + if (options.output != null) { + Files.write(options.output, baos.toByteArray()); + } + } catch (IOException e) { + LOGGER.warn("Could not write hypothesis", e); + } + } + + private Experiment buildExperiment(LearningAlgorithm learner, + EquivalenceOracle eqo, + Alphabet alphabet, + InputModelSerializer serializer, + Options options) { + if (learner instanceof Resumable r) { + return new SnapshottingExperiment<>(learner, r, eqo, alphabet, serializer, options); + } else { + if (options.resumeFrom != null || options.snapshotDir != null) { + throw new IllegalArgumentException(String.format( + "Resuming learning processes is not supported by '%s' ('%s')", + options.learner, + options.type)); + } + return new Experiment<>(learner, eqo, alphabet, serializer); + } + } + + protected abstract OR getCounter(OR delegate, String id); +} diff --git a/cli/src/main/java/de/learnlib/cli/util/AdaptiveRunner.java b/cli/src/main/java/de/learnlib/cli/util/AdaptiveRunner.java new file mode 100644 index 000000000..b36d4a920 --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/util/AdaptiveRunner.java @@ -0,0 +1,46 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.util; + +import java.util.function.BiFunction; +import java.util.function.Function; + +import de.learnlib.cli.option.Options; +import de.learnlib.filter.statistic.oracle.CounterAdaptiveQueryOracle; +import de.learnlib.oracle.AdaptiveMembershipOracle; +import de.learnlib.oracle.EquivalenceOracle; +import net.automatalib.alphabet.Alphabet; +import net.automatalib.automaton.concept.FiniteRepresentation; +import net.automatalib.serialization.InputModelSerializer; +import net.automatalib.ts.simple.SimpleTS; +import net.automatalib.word.Word; + +public class AdaptiveRunner, M extends SimpleTS & FiniteRepresentation, I, O> + extends AbstractRunner, AdaptiveMembershipOracle> { + + public AdaptiveRunner(Function alphabetCreator, + BiFunction> mqoCreator, + Function, AdaptiveMembershipOracle>> learnerCreator, + BiFunction, EquivalenceOracle>> eqoCreator, + Function> serializerCreator) { + super(alphabetCreator, mqoCreator, learnerCreator, eqoCreator, serializerCreator); + } + + @Override + protected AdaptiveMembershipOracle getCounter(AdaptiveMembershipOracle delegate, String id) { + return new CounterAdaptiveQueryOracle<>(delegate, id); + } +} diff --git a/cli/src/main/java/de/learnlib/cli/util/Constructor.java b/cli/src/main/java/de/learnlib/cli/util/Constructor.java new file mode 100644 index 000000000..d19a8887e --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/util/Constructor.java @@ -0,0 +1,59 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.util; + +import de.learnlib.algorithm.LearningAlgorithm; +import de.learnlib.cli.adapter.ProceduralDFAAdapter; +import de.learnlib.cli.adapter.ProceduralMealyAdapter; +import de.learnlib.oracle.AdaptiveMembershipOracle; +import de.learnlib.oracle.MembershipOracle; +import net.automatalib.alphabet.Alphabet; +import net.automatalib.automaton.fsa.DFA; +import net.automatalib.automaton.transducer.MealyMachine; +import net.automatalib.word.Word; + +@FunctionalInterface +public interface Constructor, M, I, D, OR> { + + LearningAlgorithm constructLearner(A alphabet, OR oracle); + + @FunctionalInterface + interface PresetConstructor, M, I, D> + extends Constructor> { + + } + + @FunctionalInterface + interface AdaptiveConstructor, M, I, O> + extends Constructor, AdaptiveMembershipOracle> { + + } + + @FunctionalInterface + interface MealyConstructor, I, O> + extends PresetConstructor, I, Word> { + + @Override + ProceduralMealyAdapter constructLearner(A alphabet, MembershipOracle> oracle); + } + + @FunctionalInterface + interface DFAConstructor, I> extends PresetConstructor, I, Boolean> { + + @Override + ProceduralDFAAdapter constructLearner(A alphabet, MembershipOracle oracle); + } +} diff --git a/cli/src/main/java/de/learnlib/cli/util/PresetRunner.java b/cli/src/main/java/de/learnlib/cli/util/PresetRunner.java new file mode 100644 index 000000000..2eb80c695 --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/util/PresetRunner.java @@ -0,0 +1,45 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.util; + +import java.util.function.BiFunction; +import java.util.function.Function; + +import de.learnlib.cli.option.Options; +import de.learnlib.filter.statistic.oracle.CounterOracle; +import de.learnlib.oracle.EquivalenceOracle; +import de.learnlib.oracle.MembershipOracle; +import net.automatalib.alphabet.Alphabet; +import net.automatalib.automaton.concept.FiniteRepresentation; +import net.automatalib.serialization.InputModelSerializer; +import net.automatalib.ts.simple.SimpleTS; + +public class PresetRunner, M extends SimpleTS & FiniteRepresentation, I, D> + extends AbstractRunner> { + + public PresetRunner(Function alphabetCreator, + BiFunction> mqoCreator, + Function>> learnerCreator, + BiFunction, EquivalenceOracle> eqoCreator, + Function> serializerCreator) { + super(alphabetCreator, mqoCreator, learnerCreator, eqoCreator, serializerCreator); + } + + @Override + protected MembershipOracle getCounter(MembershipOracle delegate, String id) { + return new CounterOracle<>(delegate, id); + } +} diff --git a/cli/src/main/java/de/learnlib/cli/util/Runner.java b/cli/src/main/java/de/learnlib/cli/util/Runner.java new file mode 100644 index 000000000..910aac55d --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/util/Runner.java @@ -0,0 +1,25 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.util; + +import de.learnlib.cli.option.Options; + +@FunctionalInterface +public interface Runner { + + void run(Options options); + +} diff --git a/cli/src/main/java/de/learnlib/cli/util/SnapshottingExperiment.java b/cli/src/main/java/de/learnlib/cli/util/SnapshottingExperiment.java new file mode 100644 index 000000000..bdf89f115 --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/util/SnapshottingExperiment.java @@ -0,0 +1,135 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.util; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import de.learnlib.Resumable; +import de.learnlib.algorithm.LearningAlgorithm; +import de.learnlib.cli.option.Options; +import de.learnlib.oracle.EquivalenceOracle; +import de.learnlib.util.Experiment; +import net.automatalib.alphabet.Alphabet; +import net.automatalib.automaton.concept.FiniteRepresentation; +import net.automatalib.serialization.InputModelSerializer; +import org.apache.fory.Fory; +import org.apache.fory.exception.ForyException; +import org.apache.fory.logging.LoggerFactory; +import org.apache.fory.resolver.AllowListChecker; +import org.apache.fory.resolver.AllowListChecker.CheckLevel; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; + +final class SnapshottingExperiment extends Experiment { + + private static final Fory FORY; + private static final DateTimeFormatter DTF; + + static { + // use same config as test-support to automatically test proper white-listing + final AllowListChecker checker = new AllowListChecker(); + checker.setCheckLevel(CheckLevel.STRICT); + checker.allowClass("de.learnlib.*"); + checker.allowClass("net.automatalib.*"); + FORY = Fory.builder() + .requireClassRegistration(false) + .withCodegen(false) + .withRefTracking(true) + .withTypeChecker(checker) + .withXlang(false) + .build(); + LoggerFactory.useSlf4jLogging(true); + DTF = DateTimeFormatter.ofPattern("-yyyyMMdd-HHmmss-"); + } + + private final Resumable resumable; + private final String fingerPrint; + + private final @Nullable Path resumeFrom; + private final @Nullable Path snapshotDir; + + SnapshottingExperiment(LearningAlgorithm learningAlgorithm, + Resumable resumable, + EquivalenceOracle equivalenceAlgorithm, + Alphabet inputs, + InputModelSerializer serializer, + Options options) { + super(learningAlgorithm, equivalenceAlgorithm, inputs, serializer); + + this.resumable = resumable; + this.fingerPrint = DTF.format(LocalDateTime.now()); + this.resumeFrom = options.resumeFrom; + this.snapshotDir = options.snapshotDir; + + if (this.resumeFrom != null && !this.resumeFrom.toFile().isFile()) { + throw new IllegalArgumentException(String.format("Provided resume path '%s' is not a file", + options.resumeFrom)); + } + + if (this.snapshotDir != null && !this.snapshotDir.toFile().isDirectory()) { + throw new IllegalArgumentException(String.format("Provided snapshot path '%s' is not a directory", + options.snapshotDir)); + } + } + + @Override + protected void initializeLearning() { + if (this.resumeFrom == null) { + super.initializeLearning(); + } else { + LOGGER.info("Resuming learning process from file '{}'", resumeFrom); + try { + resumLearner(this.resumable); + } catch (IOException | ForyException e) { + LOGGER.warn("Could not resume learning process. Starting from scratch...", e); + super.initializeLearning(); + } + } + } + + @RequiresNonNull("this.resumeFrom") + private void resumLearner(Resumable resumable) throws IOException { + @SuppressWarnings("unchecked") + final T state = (T) FORY.deserialize(Files.readAllBytes(resumeFrom)); + resumable.resume(state); + } + + @Override + protected void postRefinementHook() { + if (this.snapshotDir == null) { + super.postRefinementHook(); + } else { + snapshotState(this.resumable); + } + } + + @RequiresNonNull("this.snapshotDir") + private void snapshotState(Resumable resumable) { + final T suspend = resumable.suspend(); + final String round = Integer.toString(getRound() - 1); // do not count startLearning round + final String fileName = "learnlib" + fingerPrint + round + ".bin"; + LOGGER.info("Writing snapshot to file '{}'", fileName); + try { + Files.write(this.snapshotDir.resolve(fileName), FORY.serialize(suspend)); + } catch (IOException | ForyException e) { + LOGGER.warn("Could not write learner state. Continuing without...", e); + } + } +} diff --git a/cli/src/main/java/de/learnlib/cli/util/VersionProvider.java b/cli/src/main/java/de/learnlib/cli/util/VersionProvider.java new file mode 100644 index 000000000..29794c68c --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/util/VersionProvider.java @@ -0,0 +1,29 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.util; + +import java.util.ResourceBundle; + +import de.learnlib.cli.Application; +import picocli.CommandLine.IVersionProvider; + +public class VersionProvider implements IVersionProvider { + + @Override + public String[] getVersion() { + return new String[] {ResourceBundle.getBundle(Application.PROPERTIES).getString("app.version")}; + } +} diff --git a/cli/src/main/java/module-info.java b/cli/src/main/java/module-info.java new file mode 100644 index 000000000..f4671d634 --- /dev/null +++ b/cli/src/main/java/module-info.java @@ -0,0 +1,59 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +open module de.learnlib.cli { + + requires de.learnlib.algorithm.adt; + requires de.learnlib.algorithm.dhc; + requires de.learnlib.algorithm.kv; + requires de.learnlib.algorithm.lambda; + requires de.learnlib.algorithm.lsharp; + requires de.learnlib.algorithm.lstar; + requires de.learnlib.algorithm.nlstar; + requires de.learnlib.algorithm.observationpack; + requires de.learnlib.algorithm.observationpack.vpa; + requires de.learnlib.algorithm.procedural; + requires de.learnlib.algorithm.sparse; + requires de.learnlib.algorithm.ttt; + requires de.learnlib.algorithm.ttt.vpa; + requires de.learnlib.api; + requires de.learnlib.common.counterexample; + requires de.learnlib.common.util; + requires de.learnlib.filter.cache; + requires de.learnlib.filter.statistic; + requires de.learnlib.oracle.equivalence; + requires de.learnlib.oracle.membership; + requires de.learnlib.oracle.parallelism; + + requires net.automatalib.api; + requires net.automatalib.common.util; + requires net.automatalib.core; + requires net.automatalib.serialization.aut; + requires net.automatalib.serialization.ba; + requires net.automatalib.serialization.dot; + requires net.automatalib.serialization.learnlibv2; + requires net.automatalib.serialization.mata; + requires net.automatalib.serialization.saf; + requires net.automatalib.serialization.taf; + requires net.automatalib.util; + + requires ch.qos.logback.classic; + requires info.picocli; + requires org.apache.fory.core; + requires org.slf4j; + + requires static org.checkerframework.checker.qual; + +} diff --git a/cli/src/main/resources/application.properties b/cli/src/main/resources/application.properties new file mode 100644 index 000000000..e02ffdec6 --- /dev/null +++ b/cli/src/main/resources/application.properties @@ -0,0 +1,79 @@ +# Copyright (C) 2013-2026 TU Dortmund University +# This file is part of LearnLib . +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +app.version=${project.version} + +option.args=Additional arguments to pass to the SUL(s). +option.cache=Use a query cache. +option.delim=The regexp used to split responses of the SUL(s) into individual output symbols. For reference, see java.util.regex.Pattern. +option.eqo=The strategy to search for counterexamples. Use repeatedly to build a chain of equivalence oracles. Strategies can be parameterized via the specific '--eqo-*' flags. Possible values are: ${COMPLETION-CANDIDATES}. +option.format=The output format to use for printing the final hypothesis model. Possible values are: ${COMPLETION-CANDIDATES}. +option.learner=The learning algorithm to use. Possible values are: ${COMPLETION-CANDIDATES}. +option.output=The output file to write the final hypothesis model to. +option.reset=Specifies the reset symbol. If provided, the SUL(s) will be treated as 'stateful' which results in query symbols being applied one-by-one in subsequent invocations. The reset symbol will be used to reset the application state between queries. +option.resume=Resume the learning process from a previous snapshot. You must ensure that the same learner and alphabet definitions from the previous run are used. +option.snapshot=Directory for storing snapshots of the learner after each refinement. May be used by '--resume-from' to resume the learning process from a previous snapshot in case there have been issues with the SUL(s). +option.stats=Enable the collection of statistics which will be printed at the end of the learning process. +option.stdin=Apply query symbols via standard-in as opposed to argument lists to the SUL(s). +option.sul=The path(s) to the SUL used for answering queries. If more than one path is specified, queries will be answered in parallel by distributing queries to the provided SULs. Note that all SULs must behave equivalent for consistency reasons. +option.type=The model type to infer. Possible values are: ${COMPLETION-CANDIDATES}. +option.verbose=Increase verbosity. Each repetition increases the log level from (initially) INFO to DEBUG to TRACE. + +param.eqo.heading=%nParameters for equivalence oracles:%n + +param.eqo.kway-s.combinationMethod=The 'combinationMethod' parameter of the 'KWayStateCoverEQOracle'. Possible values are: ${COMPLETION-CANDIDATES}. +param.eqo.kway-s.k=The 'k' parameter of the 'KWayStateCoverEQOracle'. +param.eqo.kway-s.randomWalkLen=The 'randomWalkLen' parameter of the 'KWayStateCoverEQOracle'. +param.eqo.kway-s.seed=The seed of the 'random' parameter of the 'KWayStateCoverEQOracle'. + +param.eqo.kway-t.generationMethod=The 'generationMethod' parameter of the 'KWayTransitionCoverEQOracle'. Possible values are: ${COMPLETION-CANDIDATES}. +param.eqo.kway-t.k=The 'k' parameter of the 'KWayTransitionCoverEQOracle'. +param.eqo.kway-t.maxNumberOfSteps=The 'maxNumberOfSteps' parameter of the 'KWayTransitionCoverEQOracle'. +param.eqo.kway-t.maxPathLen=The 'maxPathLen' parameter of the 'KWayTransitionCoverEQOracle'. +param.eqo.kway-t.numGeneratePaths=The 'numGeneratePaths' parameter of the 'KWayTransitionCoverEQOracle'. +param.eqo.kway-t.optimizationMetric=The 'optimizationMetric' parameter of the 'KWayTransitionCoverEQOracle'. Possible values are: ${COMPLETION-CANDIDATES}. +param.eqo.kway-t.randomWalkLen=The 'randomWalkLen' parameter of the 'KWayTransitionCoverEQOracle'. +param.eqo.kway-t.seed=The seed of the 'random' parameter of the 'KWayTransitionCoverEQOracle'. + +param.eqo.random-w.bound=The 'bound' parameter of the 'RandomWMethodEQOracle'. +param.eqo.random-w.minimalSize=The 'minimalSize' parameter of the 'RandomWMethodEQOracle'. +param.eqo.random-w.rndLength=The 'rndLength' parameter of the 'RandomWMethodEQOracle'. +param.eqo.random-w.seed=The seed of the 'random' parameter of the 'RandomWMethodEQOracle'. + +param.eqo.random-wp.bound=The 'bound' parameter of the 'RandomWpMethodEQOracle'. +param.eqo.random-wp.minimalSize=The 'minimalSize' parameter of the 'RandomWpMethodEQOracle'. +param.eqo.random-wp.rndLength=The 'rndLength' parameter of the 'RandomWpMethodEQOracle'. +param.eqo.random-wp.seed=The seed of the 'random' parameter of the 'RandomWpMethodEQOracle'. + +param.eqo.random.maxLength=The 'maxLength' parameter of the 'RandomWordsEQOracle'. +param.eqo.random.maxTests=The 'maxTests' parameter of the 'RandomWordsEQOracle'. +param.eqo.random.minLength=The 'minLength' parameter of the 'RandomWordsEQOracle'. +param.eqo.random.seed=The seed of the 'random' parameter of the 'RandomWordsEQOracle'. + +param.eqo.sample=Provide an explicit trace that should be used for equivalence testing. Use repeatedly to provide multiple traces. +param.eqo.sample.split=The regexp used to split individual symbols of a trace. For reference, see java.util.regex.Pattern. + +param.eqo.w.expectedSize=The 'expectedSize' parameter for the 'WMethodEQOracle'. +param.eqo.w.lookahead=The 'lookahead' parameter for the 'WMethodEQOracle'. + +param.eqo.wp.expectedSize=The 'expectedSize' parameter for the 'WpMethodEQOracle'. +param.eqo.wp.lookahead=The 'lookahead' parameter for the 'WpMethodEQOracle'. + +param.symbol.cf.call.description=Define a call symbol. Use repeatedly to define multiple symbols. +param.symbol.cf.heading=%nSymbol definitions for learning context-free systems:%n +param.symbol.cf.int.description=Define an internal symbol. Use repeatedly to define multiple symbols. +param.symbol.cf.ret.description=Define a return symbol. Use repeatedly to define multiple symbols. +param.symbol.reg.description=Define an input symbol. Use repeatedly to define multiple symbols. +param.symbol.reg.heading=%nSymbol definitions for learning regular systems:%n diff --git a/cli/src/main/resources/logback.xml b/cli/src/main/resources/logback.xml new file mode 100644 index 000000000..18c501741 --- /dev/null +++ b/cli/src/main/resources/logback.xml @@ -0,0 +1,28 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %25.25(%logger{25}) - %msg %n + + + + + + + diff --git a/cli/src/test/java/de/learnlib/cli/AbstractPythonTest.java b/cli/src/test/java/de/learnlib/cli/AbstractPythonTest.java new file mode 100644 index 000000000..2041dcc58 --- /dev/null +++ b/cli/src/test/java/de/learnlib/cli/AbstractPythonTest.java @@ -0,0 +1,60 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +import net.automatalib.common.util.process.ProcessUtil; +import org.testng.SkipException; +import org.testng.annotations.BeforeClass; + +public abstract class AbstractPythonTest { + + private static final boolean AVAILABLE; + public static final String PROGRAM; + + static { + String path = ""; + boolean available = false; + + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + // if python is available, we can also use it to give us the absolute path to its interpreter + if (ProcessUtil.invokeProcess(new String[] {"python3", "-c", "import sys; print(sys.executable, end=\"\")"}, + null, + baos, + OutputStream.nullOutputStream()) == 0) { + path = baos.toString(StandardCharsets.UTF_8); + available = true; + } + } catch (IOException | InterruptedException ignored) { + // use defaults + } + + AVAILABLE = available; + PROGRAM = path; + } + + @BeforeClass + public void setUp() { + if (!AVAILABLE) { + throw new SkipException("python3 not supported"); + } + } + +} diff --git a/cli/src/test/java/de/learnlib/cli/ApplicationIT.java b/cli/src/test/java/de/learnlib/cli/ApplicationIT.java new file mode 100644 index 000000000..f331a9841 --- /dev/null +++ b/cli/src/test/java/de/learnlib/cli/ApplicationIT.java @@ -0,0 +1,257 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringWriter; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Objects; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import de.learnlib.cli.util.AbstractRunner; +import de.learnlib.filter.statistic.oracle.CounterAdaptiveQueryOracle; +import de.learnlib.filter.statistic.oracle.CounterOracle; +import de.learnlib.statistic.Statistics; +import de.learnlib.statistic.StatisticsService; +import net.automatalib.common.util.IOUtil; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import org.testng.SkipException; +import org.testng.annotations.Test; +import picocli.CommandLine; + +public class ApplicationIT extends AbstractPythonTest { + + public static final String STATELESS = getPathToScript("/sul/stateless.py"); + public static final String STATELESS_BROKEN = getPathToScript("/sul/stateless_broken.py"); + public static final String STATELESS_LARGE = getPathToScript("/sul/stateless_large.py"); + public static final String STATELESS_SPA = getPathToScript("/sul/spa.py"); + public static final String STATELESS_SBA = getPathToScript("/sul/sba.py"); + public static final String STATEFUL = getPathToScript("/sul/stateful.py"); + + private static String getPathToScript(String script) { + final URL resource = Objects.requireNonNull(ApplicationIT.class.getResource(script)); + try { + return Paths.get(resource.toURI()).toFile().getAbsolutePath(); + } catch (URISyntaxException e) { + throw new SkipException("Error while loading script " + script); + } + } + + @Test + public void testVerbosity() { + final Logger root = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); + final Level oldLevel = root.getLevel(); + + final Application app = new Application(); + final CommandLine cmd = new CommandLine(app); + + int exitCode = cmd.execute("-v", STATELESS, "-sa", "-eSAMPLE", "--eqo-sample=a b"); + + Assert.assertEquals(exitCode, 0); + Assert.assertEquals(root.getLevel(), Level.DEBUG); + + exitCode = cmd.execute("-vv", STATELESS, "-sa", "-eSAMPLE", "--eqo-sample=a b"); + + Assert.assertEquals(exitCode, 0); + Assert.assertEquals(root.getLevel(), Level.TRACE); + + root.setLevel(oldLevel); + } + + @Test + public void testStatelessDFA() throws IOException { + checkRegularExecution(new String[] {PROGRAM, "--args", STATELESS, "-lL_STAR", "-eSAMPLE", "--eqo-sample=a b"}, + "/ser/dfa.dot"); + } + + @Test + public void testMealyPreset() throws IOException { + checkRegularExecution(new String[] {PROGRAM, + "--args", + STATELESS, + "-lL_STAR", + "-tMEALY", + "-eSAMPLE", + "--eqo-sample=a b"}, "/ser/mealy.dot"); + } + + @Test + public void testMealyAdaptive() throws IOException { + checkRegularExecution(new String[] {PROGRAM, + "--args", + STATELESS, + "-tMEALY", + "-lL_SHARP", + "-eSAMPLE", + "--eqo-sample=a b", + "--reset=reset"}, "/ser/mealy.dot"); + } + + @Test + public void testStatelessNFA() throws IOException { + checkRegularExecution(new String[] {PROGRAM, + "--args", + STATELESS, + "-tNFA", + "-lNL_STAR", + "-eSAMPLE", + "--eqo-sample=a b"}, "/ser/dfa.dot"); + } + + @Test + public void testStatelessSBA() throws IOException { + checkProceduralExecution(new String[] {PROGRAM, + "--args", + STATELESS_SBA, + "-lL_STAR", + "-tSBA", + "-eSAMPLE", + "--eqo-sample=S a R"}, null); + } + + @Test + public void testStatelessSPA() throws IOException { + checkProceduralExecution(new String[] {PROGRAM, + "--args", + STATELESS_SPA, + "-lL_STAR", + "-tSPA", + "-eSAMPLE", + "--eqo-sample=S a R"}, "/ser/spa.dot"); + } + + @Test + public void testStatelessSPMM() throws IOException { + checkProceduralExecution(new String[] {PROGRAM, + "--args", + STATELESS_SBA, + "-lL_STAR", + "-tSPMM", + "-eSAMPLE", + "--eqo-sample=S a R", + "-d=\\s"}, "/ser/spmm.dot"); + } + + @Test + public void testStatelessVPA() throws IOException { + checkProceduralExecution(new String[] {PROGRAM, + "--args", + STATELESS_SPA, + "-tVPA", + "-eSAMPLE", + "--eqo-sample=S a R"}, null); + } + + @Test + public void testStatistics() throws IOException { + checkRegularExecution(new String[] {PROGRAM, + "--args", + STATELESS, + "-lL_STAR", + "-eSAMPLE", + "--eqo-sample=abab", + "--stats"}, null); + + StatisticsService statistics = Statistics.getService(); + Assert.assertTrue(statistics.getCount(CounterOracle.KEY_SYMBOL.withId(AbstractRunner.EQO_KEY)).isPresent()); + Assert.assertTrue(statistics.getCount(CounterOracle.KEY_SYMBOL.withId(AbstractRunner.LEARNER_KEY)).isPresent()); + statistics.clear(); + } + + @Test + public void testAdaptiveStatistics() throws IOException { + checkRegularExecution(new String[] {PROGRAM, + "--args", + STATEFUL, + "-tMEALY", + "-lL_SHARP", + "-eSAMPLE", + "--eqo-sample=abab", + "--reset=reset", + "--stats"}, null); + + StatisticsService statistics = Statistics.getService(); + Assert.assertTrue(statistics.getCount(CounterAdaptiveQueryOracle.KEY_SYMBOL.withId(AbstractRunner.EQO_KEY)) + .isPresent()); + Assert.assertTrue(statistics.getCount(CounterAdaptiveQueryOracle.KEY_SYMBOL.withId(AbstractRunner.LEARNER_KEY)) + .isPresent()); + statistics.clear(); + } + + private void checkRegularExecution(String[] params, @Nullable String resource) throws IOException { + Path out = Files.createTempFile("automatalib", ""); + out.toFile().deleteOnExit(); + + final String[] args = Arrays.copyOf(params, params.length + 4); + args[args.length - 4] = "-sa"; + args[args.length - 3] = "-sb"; + args[args.length - 2] = "-o"; + args[args.length - 1] = out.toAbsolutePath().toString(); + + final Application app = new Application(); + final CommandLine cmd = new CommandLine(app); + + final int exitCode = cmd.execute(args); + + Assert.assertEquals(exitCode, 0); + + if (resource != null) { + checkOutputs(out, resource); + } + } + + private void checkProceduralExecution(String[] params, String resource) throws IOException { + Path out = Files.createTempFile("automatalib", ""); + out.toFile().deleteOnExit(); + + final String[] args = Arrays.copyOf(params, params.length + 6); + args[args.length - 6] = "--call=S"; + args[args.length - 5] = "--int=a"; + args[args.length - 4] = "--int=b"; + args[args.length - 3] = "--ret=R"; + args[args.length - 2] = "-o"; + args[args.length - 1] = out.toAbsolutePath().toString(); + + final Application app = new Application(); + final CommandLine cmd = new CommandLine(app); + + final int exitCode = cmd.execute(args); + + Assert.assertEquals(exitCode, 0); + if (resource != null) { + checkOutputs(out, resource); + } + } + + public static void checkOutputs(Path output, String resource) throws IOException { + final StringWriter expectedWriter = new StringWriter(); + + try (Reader reader = IOUtil.asBufferedUTF8Reader(ApplicationIT.class.getResourceAsStream(resource))) { + reader.transferTo(expectedWriter); + Assert.assertEquals(Files.readString(output), expectedWriter.toString()); + } + } + +} diff --git a/cli/src/test/java/de/learnlib/cli/CheckBinary.java b/cli/src/test/java/de/learnlib/cli/CheckBinary.java new file mode 100644 index 000000000..9bb2c3ce2 --- /dev/null +++ b/cli/src/test/java/de/learnlib/cli/CheckBinary.java @@ -0,0 +1,67 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.List; + +import net.automatalib.common.util.process.ProcessUtil; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * This is an integration test for invoking the final binary. It does not match the regular naming conventions of the + * surefire- or failsafe-plugin, because it should only be explicitly called by the failsafe-plugin when the "cli" + * profile is active and the native binary is actually built. + */ +public class CheckBinary { + + @Test + public void testInvokeBinary() throws IOException, InterruptedException { + + final File bin = + new File(System.getProperty("learnlib.binary.path", "target/maven-jlink/default/bin/learnlib")); + final String sul = ApplicationIT.STATELESS; + final File snapshot = Files.createTempDirectory("learnlib-snapshot").toFile(); + snapshot.deleteOnExit(); + + final List argLine = Arrays.asList(bin.getAbsolutePath(), + "-tDFA", + "-lL_STAR", + "-sa", + "-sb", + "-eSAMPLE", + "--eqo-sample=a a", + "--eqo-sample=a b", + "--eqo-sample=b", + "--cache", + "--stats", + "--snapshot-dir", + snapshot.toString(), + sul); + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + final int exitCode = ProcessUtil.invokeProcess(argLine, null, OutputStream.nullOutputStream(), baos); + + Assert.assertEquals(exitCode, 0, baos.toString(StandardCharsets.UTF_8)); + } +} diff --git a/cli/src/test/java/de/learnlib/cli/SnapshotIT.java b/cli/src/test/java/de/learnlib/cli/SnapshotIT.java new file mode 100644 index 000000000..c09486d3b --- /dev/null +++ b/cli/src/test/java/de/learnlib/cli/SnapshotIT.java @@ -0,0 +1,208 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.testng.Assert; +import org.testng.annotations.Test; +import picocli.CommandLine; +import picocli.CommandLine.IExecutionExceptionHandler; + +public class SnapshotIT { + + private final CommandLine cmd; + + public SnapshotIT() { + cmd = new CommandLine(new Application()); + prepareCommandLine(cmd); + } + + private void prepareCommandLine(CommandLine cmd) { + final IExecutionExceptionHandler defaultHandler = cmd.getExecutionExceptionHandler(); + + cmd.setExecutionExceptionHandler((ex, commandLine, fullParseResult) -> { + Assert.assertTrue(ex instanceof IllegalArgumentException, ex.toString()); + return defaultHandler.handleExecutionException(ex, commandLine, fullParseResult); + }); + cmd.setParameterExceptionHandler((ex, args) -> { + Assert.fail("The application should not fail because of wrong parameters"); + return 0; + }); + cmd.setErr(new PrintWriter(OutputStream.nullOutputStream())); + } + + @Test + public void testNonResumableLearner() throws IOException { + final File resume = Files.createTempFile("learnlib-resume", "").toFile(); + final File snapshot = Files.createTempDirectory("learnlib-snapshot").toFile(); + + resume.deleteOnExit(); + snapshot.deleteOnExit(); + int exitCode; + + exitCode = cmd.execute(ApplicationIT.PROGRAM, + "--args", + ApplicationIT.STATELESS, + "-sa", + "-sb", + "-tMEALY", + "-lSPARSE", + "--resume-from", + resume.toString()); + Assert.assertTrue(exitCode > 0); + + exitCode = cmd.execute(ApplicationIT.PROGRAM, + "--args", + ApplicationIT.STATELESS, + "-sa", + "-sb", + "-tMEALY", + "-lSPARSE", + "--snapshot-dir", + snapshot.toString()); + Assert.assertTrue(exitCode > 0); + + exitCode = cmd.execute(ApplicationIT.PROGRAM, + "--args", + ApplicationIT.STATELESS, + "-sa", + "-sb", + "-tMEALY", + "-lSPARSE", + "--resume-from", + resume.toString(), + "--snapshot-dir", + snapshot.toString()); + Assert.assertTrue(exitCode > 0); + } + + @Test + public void testInvalidSnapshotProperties() throws IOException { + final File resume = Files.createTempDirectory("learnlib-resume").toFile(); + final File snapshot = Files.createTempFile("learnlib-snapshot", "").toFile(); + + resume.deleteOnExit(); + snapshot.deleteOnExit(); + + int exitCode; + + exitCode = cmd.execute(ApplicationIT.PROGRAM, + "--args", + ApplicationIT.STATELESS, + "-sa", + "-sb", + "--resume-from", + resume.toString()); + Assert.assertTrue(exitCode > 0); + + exitCode = cmd.execute(ApplicationIT.PROGRAM, + "--args", + ApplicationIT.STATELESS, + "-sa", + "-sb", + "--snapshot-dir", + snapshot.toString()); + Assert.assertTrue(exitCode > 0); + + exitCode = cmd.execute(ApplicationIT.PROGRAM, + "--args", + ApplicationIT.STATELESS, + "-sa", + "-sb", + "--resume-from", + resume.toString(), + "--snapshot-dir", + snapshot.toString()); + Assert.assertTrue(exitCode > 0); + } + + @Test + public void testSuspendResume() throws IOException { + final File snapshot = Files.createTempDirectory("learnlib-snapshot").toFile(); + final File output = Files.createTempFile("learnlib-output", "").toFile(); + + snapshot.deleteOnExit(); + output.deleteOnExit(); + int exitCode; + + // run regular scenario with snapshotting + exitCode = cmd.execute(ApplicationIT.PROGRAM, + "--args", + ApplicationIT.STATELESS_LARGE, + "-sa", + "-sb", + "-tMEALY", + "-eSAMPLE", + "--eqo-sample=a a a a", + "--snapshot-dir", + snapshot.toString()); + Assert.assertEquals(exitCode, 0); + + final List files = Files.list(snapshot.toPath()).toList(); + + Assert.assertEquals(files.size(), 1); + final File resume = files.get(0).toFile(); + resume.deleteOnExit(); + + // use cmd once https://github.com/remkop/picocli/issues/2066 is fixed + final CommandLine cmd2 = new CommandLine(new Application()); + prepareCommandLine(cmd2); + // resume from snapshot with broken SUL + // with an empty SampleSet oracle, no equivalence queries should be posed + // the final hypothesis should be completely constructed from resuming + exitCode = cmd2.execute(ApplicationIT.PROGRAM, + "--args", + ApplicationIT.STATELESS_BROKEN, + "-sa", + "-sb", + "-tMEALY", + "-eSAMPLE", + "--resume-from", + resume.toString(), + "-o", + output.toString()); + Assert.assertEquals(exitCode, 0); + + ApplicationIT.checkOutputs(output.toPath(), "/ser/mealy_large.dot"); + } + + @Test + public void testRegularExecution() throws IOException { + final File output = Files.createTempFile("learnlib-output", "").toFile(); + output.deleteOnExit(); + + // run regular scenario with snapshotting + final int exitCode = cmd.execute(ApplicationIT.PROGRAM, + "--args", + ApplicationIT.STATELESS_LARGE, + "-sa", + "-sb", + "-tMEALY", + "-eSAMPLE", + "--eqo-sample=a a a a", + "-o", + output.toString()); + Assert.assertEquals(exitCode, 0); + ApplicationIT.checkOutputs(output.toPath(), "/ser/mealy_large.dot"); + } +} diff --git a/cli/src/test/java/de/learnlib/cli/factory/AlphabetFactoryTest.java b/cli/src/test/java/de/learnlib/cli/factory/AlphabetFactoryTest.java new file mode 100644 index 000000000..cebd0c458 --- /dev/null +++ b/cli/src/test/java/de/learnlib/cli/factory/AlphabetFactoryTest.java @@ -0,0 +1,99 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.factory; + +import java.io.OutputStream; +import java.io.PrintWriter; +import java.util.Arrays; + +import de.learnlib.cli.Application; +import de.learnlib.cli.ApplicationIT; +import de.learnlib.cli.util.Util; +import net.automatalib.alphabet.impl.Alphabets; +import net.automatalib.alphabet.impl.DefaultProceduralInputAlphabet; +import net.automatalib.alphabet.impl.DefaultVPAlphabet; +import org.testng.Assert; +import org.testng.annotations.Test; +import picocli.CommandLine; +import picocli.CommandLine.ParameterException; + +public class AlphabetFactoryTest { + + @Test + public void testSymbolDefinitions() { + final Application app = new Application(); + final CommandLine cmd = new CommandLine(app); + cmd.setErr(new PrintWriter(OutputStream.nullOutputStream())); + + var regular = new String[] {ApplicationIT.STATELESS, "-sb", "-sa"}; + var procedural = new String[] {ApplicationIT.STATELESS, "--call=a", "--int=b1", "--int=b2", "--ret=c"}; + var pushdown = new String[] {ApplicationIT.STATELESS, "--call=a", "--int=b", "--ret=c2", "--ret=c1"}; + + var regularOptions = Util.parseOptions(cmd, regular); + Assert.assertEquals(Alphabets.fromArray("b", "a"), AlphabetFactory.getRegularAlphabet(regularOptions)); + + var proceduralOptions = Util.parseOptions(cmd, procedural); + Assert.assertEquals(new DefaultProceduralInputAlphabet<>(Alphabets.fromArray("b1", "b2"), + Alphabets.singleton("a"), + "c"), + AlphabetFactory.getProceduralAlphabet(proceduralOptions)); + + var pushdownOptions = Util.parseOptions(cmd, pushdown); + Assert.assertEquals(new DefaultVPAlphabet<>(Alphabets.singleton("b"), + Alphabets.singleton("a"), + Alphabets.fromArray("c2", "c1")), + AlphabetFactory.getVPAlphabet(pushdownOptions)); + + var bad = new String[][] {// no symbols + {ApplicationIT.STATELESS}, + // regular symbols on context-free learner + {ApplicationIT.STATELESS, "-sa", "-tSBA"}, + // regular symbols on context-free learner + {ApplicationIT.STATELESS, "-sa", "-tVPA"}, + // incomplete context-free symbols + {ApplicationIT.STATELESS, "--call=a", "-tVPA"}, + // incomplete context-free symbols + {ApplicationIT.STATELESS, "--int=a", "-tSPMM"}, + // incomplete context-free symbols + {ApplicationIT.STATELESS, "--ret=a", "-tVPA"}, + // context-free symbols on regular learner + {ApplicationIT.STATELESS, "--call=a", "--int=b", "--ret=c", "-tMEALY"}, + // multiple return symbols on a procedural learner + {ApplicationIT.STATELESS, "--call=a", "--int=b", "--ret=c", "--ret=d", "-tSBA"}}; + + for (String[] args : bad) { + try { + var options = Util.parseOptions(cmd, args); + switch (options.type) { + case DFA: + case MEALY: + case NFA: + AlphabetFactory.getRegularAlphabet(options); + case SBA: + case SPA: + case SPMM: + AlphabetFactory.getProceduralAlphabet(options); + case VPA: + AlphabetFactory.getVPAlphabet(options); + default: + Assert.fail(Arrays.toString(args)); + } + } catch (ParameterException | NullPointerException | IllegalArgumentException ignored) { + // ignore + } + } + } +} diff --git a/cli/src/test/java/de/learnlib/cli/factory/EQOFactoryTest.java b/cli/src/test/java/de/learnlib/cli/factory/EQOFactoryTest.java new file mode 100644 index 000000000..983f63381 --- /dev/null +++ b/cli/src/test/java/de/learnlib/cli/factory/EQOFactoryTest.java @@ -0,0 +1,626 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.factory; + +import java.io.OutputStream; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.stream.Stream; + +import de.learnlib.cli.Application; +import de.learnlib.cli.ApplicationIT; +import de.learnlib.cli.option.EQOracle; +import de.learnlib.cli.option.Options; +import de.learnlib.cli.util.AcceptorNullOracle; +import de.learnlib.cli.util.AdaptiveNullOracle; +import de.learnlib.cli.util.TransducerNullOracle; +import de.learnlib.cli.util.Util; +import de.learnlib.oracle.equivalence.EQOracleChain; +import de.learnlib.oracle.equivalence.KWayStateCoverEQOracle; +import de.learnlib.oracle.equivalence.KWayTransitionCoverEQOracle; +import de.learnlib.oracle.equivalence.RandomWMethodEQOracle; +import de.learnlib.oracle.equivalence.RandomWordsEQOracle; +import de.learnlib.oracle.equivalence.RandomWpMethodEQOracle; +import de.learnlib.oracle.equivalence.SampleSetEQOracle; +import de.learnlib.oracle.equivalence.WMethodEQOracle; +import de.learnlib.oracle.equivalence.WpMethodEQOracle; +import de.learnlib.oracle.equivalence.vpa.RandomWellMatchedWordsEQOracle; +import net.automatalib.automaton.UniversalDeterministicAutomaton.RegularAutomaton; +import net.automatalib.automaton.fsa.NFA; +import net.automatalib.automaton.procedural.SBA; +import net.automatalib.automaton.procedural.SPA; +import net.automatalib.automaton.procedural.SPMM; +import net.automatalib.automaton.vpa.OneSEVPA; +import net.automatalib.common.util.random.RandomUtil; +import net.automatalib.util.automaton.conformance.KWayStateCoverTestsIterator.CombinationMethod; +import net.automatalib.util.automaton.conformance.KWayTransitionCoverTestsIterator.GenerationMethod; +import net.automatalib.util.automaton.conformance.KWayTransitionCoverTestsIterator.OptimizationMetric; +import net.automatalib.word.Word; +import org.mockito.MockedConstruction; +import org.mockito.Mockito; +import org.testng.Assert; +import org.testng.annotations.Test; +import picocli.CommandLine; + +public class EQOFactoryTest { + + private final CommandLine cmd; + + public EQOFactoryTest() { + cmd = new CommandLine(new Application()); + cmd.setErr(new PrintWriter(OutputStream.nullOutputStream())); + } + + @Test + public void testRegularEQOs() { + final Set remaining = EnumSet.allOf(EQOracle.class); + final Options options = buildOptions(remaining, + EQOracle.SAMPLE, + EQOracle.RANDOM, + EQOracle.KWAY_S, + EQOracle.W, + EQOracle.RANDOM_WP, + EQOracle.KWAY_T, + EQOracle.WP, + EQOracle.RANDOM_W); + + final EQOracleChain, String, Boolean> chain = + EQOFactory.getRegularOracles(options, new AcceptorNullOracle()); + var oracles = chain.getOracles(); + + Assert.assertEquals(oracles.size(), 8); + + Assert.assertTrue(oracles.get(0) instanceof SampleSetEQOracle); + Assert.assertTrue(oracles.get(1) instanceof RandomWordsEQOracle); + Assert.assertTrue(oracles.get(2) instanceof KWayStateCoverEQOracle); + Assert.assertTrue(oracles.get(3) instanceof WMethodEQOracle); + Assert.assertTrue(oracles.get(4) instanceof RandomWpMethodEQOracle); + Assert.assertTrue(oracles.get(5) instanceof KWayTransitionCoverEQOracle); + Assert.assertTrue(oracles.get(6) instanceof WpMethodEQOracle); + Assert.assertTrue(oracles.get(7) instanceof RandomWMethodEQOracle); + + Assert.assertTrue(remaining.isEmpty()); + } + + @Test + public void testAdaptiverEQOs() { + final Set remaining = EnumSet.allOf(EQOracle.class); + final Options options = buildOptions(remaining, + EQOracle.SAMPLE, + EQOracle.RANDOM, + EQOracle.KWAY_S, + EQOracle.W, + EQOracle.RANDOM_WP, + EQOracle.KWAY_T, + EQOracle.WP, + EQOracle.RANDOM_W); + + final EQOracleChain, String, Word> chain = + EQOFactory.getAdaptiveOracles(options, new AdaptiveNullOracle()); + var oracles = chain.getOracles(); + + Assert.assertEquals(oracles.size(), 8); + + Assert.assertTrue(oracles.get(0) instanceof SampleSetEQOracle); + Assert.assertTrue(oracles.get(1) instanceof RandomWordsEQOracle); + Assert.assertTrue(oracles.get(2) instanceof KWayStateCoverEQOracle); + Assert.assertTrue(oracles.get(3) instanceof WMethodEQOracle); + Assert.assertTrue(oracles.get(4) instanceof RandomWpMethodEQOracle); + Assert.assertTrue(oracles.get(5) instanceof KWayTransitionCoverEQOracle); + Assert.assertTrue(oracles.get(6) instanceof WpMethodEQOracle); + Assert.assertTrue(oracles.get(7) instanceof RandomWMethodEQOracle); + + Assert.assertTrue(remaining.isEmpty()); + } + + @Test + public void testNFAEQOs() { + final Set remaining = EnumSet.allOf(EQOracle.class); + final Options options = buildOptions(remaining, + EQOracle.SAMPLE, + EQOracle.RANDOM, + EQOracle.KWAY_S, + EQOracle.W, + EQOracle.RANDOM_WP, + EQOracle.KWAY_T, + EQOracle.WP, + EQOracle.RANDOM_W); + + final EQOracleChain, String, Boolean> chain = + EQOFactory.getNFAOracles(options, new AcceptorNullOracle()); + var oracles = chain.getOracles(); + + Assert.assertEquals(oracles.size(), 8); + Assert.assertTrue(remaining.isEmpty()); + } + + @Test + public void testSBAEQOs() { + final Set remaining = EnumSet.allOf(EQOracle.class); + final Options options = buildOptions(remaining, EQOracle.SAMPLE, EQOracle.RANDOM, EQOracle.W); + + final EQOracleChain, String, Boolean> chain = + EQOFactory.getSBAOracles(options, new AcceptorNullOracle()); + var oracles = chain.getOracles(); + + Assert.assertEquals(oracles.size(), 3); + Assert.assertTrue(oracles.get(0) instanceof SampleSetEQOracle); + Assert.assertTrue(oracles.get(1) instanceof RandomWellMatchedWordsEQOracle); + Assert.assertTrue(oracles.get(2) instanceof de.learnlib.oracle.equivalence.sba.WMethodEQOracle); + + for (EQOracle oracle : remaining) { + final Options opt = buildOptions(remaining, oracle); + Assert.assertThrows(() -> EQOFactory.getSBAOracles(opt, new AcceptorNullOracle())); + } + } + + @Test + public void testSPAEQOs() { + final Set remaining = EnumSet.allOf(EQOracle.class); + final Options options = buildOptions(remaining, EQOracle.SAMPLE, EQOracle.WP, EQOracle.RANDOM, EQOracle.W); + + final EQOracleChain, String, Boolean> chain = + EQOFactory.getSPAOracles(options, new AcceptorNullOracle()); + var oracles = chain.getOracles(); + + Assert.assertEquals(oracles.size(), 4); + Assert.assertTrue(oracles.get(0) instanceof SampleSetEQOracle); + Assert.assertTrue(oracles.get(1) instanceof de.learnlib.oracle.equivalence.spa.WpMethodEQOracle); + Assert.assertTrue(oracles.get(2) instanceof RandomWellMatchedWordsEQOracle); + Assert.assertTrue(oracles.get(3) instanceof de.learnlib.oracle.equivalence.spa.WMethodEQOracle); + + for (EQOracle oracle : remaining) { + final Options opt = buildOptions(remaining, oracle); + Assert.assertThrows(() -> EQOFactory.getSPAOracles(opt, new AcceptorNullOracle())); + } + } + + @Test + public void testSPMMEQOs() { + final Set remaining = EnumSet.allOf(EQOracle.class); + final Options options = buildOptions(remaining, EQOracle.SAMPLE, EQOracle.RANDOM, EQOracle.W); + + final EQOracleChain, String, Word> chain = + EQOFactory.getSPMMOracles(options, new TransducerNullOracle()); + var oracles = chain.getOracles(); + + Assert.assertEquals(oracles.size(), 3); + Assert.assertTrue(oracles.get(0) instanceof SampleSetEQOracle); + Assert.assertTrue(oracles.get(1) instanceof RandomWellMatchedWordsEQOracle); + Assert.assertTrue(oracles.get(2) instanceof de.learnlib.oracle.equivalence.spmm.WMethodEQOracle); + + for (EQOracle oracle : remaining) { + final Options opt = buildOptions(remaining, oracle); + Assert.assertThrows(() -> EQOFactory.getSPMMOracles(opt, new TransducerNullOracle())); + } + } + + @Test + public void testVPAEQOs() { + final Set remaining = EnumSet.allOf(EQOracle.class); + final Options options = buildOptions(remaining, EQOracle.SAMPLE, EQOracle.RANDOM); + + final EQOracleChain, String, Boolean> chain = + EQOFactory.getVPAOracles(options, new AcceptorNullOracle()); + var oracles = chain.getOracles(); + + Assert.assertEquals(oracles.size(), 2); + Assert.assertTrue(oracles.get(0) instanceof SampleSetEQOracle); + Assert.assertTrue(oracles.get(1) instanceof RandomWellMatchedWordsEQOracle); + + for (EQOracle oracle : remaining) { + final Options opt = buildOptions(remaining, oracle); + Assert.assertThrows(() -> EQOFactory.getVPAOracles(opt, new AcceptorNullOracle())); + } + } + + @Test + public void testRegularParameters() { + + // Set up randomized options + final Random r = new Random(42); + final int randomBound = 1_000; + final List args = new ArrayList<>(); + + args.add(ApplicationIT.STATEFUL); + args.add(ApplicationIT.STATEFUL); + args.add("-sa"); + Arrays.stream(EQOracle.values()).map(EQOracle::name).forEach(n -> args.add("-e" + n)); + + args.add("--eqo-kway-s-combinationMethod=" + RandomUtil.choose(r, CombinationMethod.values())); + args.add("--eqo-kway-s-k=" + r.nextInt(randomBound)); + args.add("--eqo-kway-s-randomWalkLen=" + r.nextInt(randomBound)); + args.add("--eqo-kway-s-seed=" + r.nextInt(randomBound)); + + args.add("--eqo-kway-t-randomWalkLen=" + r.nextInt(randomBound)); + args.add("--eqo-kway-t-numGeneratePaths=" + r.nextInt(randomBound)); + args.add("--eqo-kway-t-maxPathLen=" + r.nextInt(randomBound)); + args.add("--eqo-kway-t-maxNumberOfSteps=" + r.nextInt(randomBound)); + args.add("--eqo-kway-t-k=" + r.nextInt(randomBound)); + args.add("--eqo-kway-t-optimizationMetric=" + RandomUtil.choose(r, OptimizationMetric.values())); + args.add("--eqo-kway-t-generationMethod=" + RandomUtil.choose(r, GenerationMethod.values())); + args.add("--eqo-kway-t-seed=" + r.nextInt(randomBound)); + + args.add("--eqo-random-minLength=" + r.nextInt(randomBound)); + args.add("--eqo-random-maxLength=" + r.nextInt(randomBound)); + args.add("--eqo-random-maxTests=" + r.nextInt(randomBound)); + args.add("--eqo-random-seed=" + r.nextInt(randomBound)); + + args.add("--eqo-random-w-minimalSize=" + r.nextInt(randomBound)); + args.add("--eqo-random-w-rndLength=" + r.nextInt(randomBound)); + args.add("--eqo-random-w-bound=" + r.nextInt(randomBound)); + args.add("--eqo-random-w-seed=" + r.nextInt(randomBound)); + + args.add("--eqo-random-wp-minimalSize=" + r.nextInt(randomBound)); + args.add("--eqo-random-wp-rndLength=" + r.nextInt(randomBound)); + args.add("--eqo-random-wp-bound=" + r.nextInt(randomBound)); + args.add("--eqo-random-wp-seed=" + r.nextInt(randomBound)); + + args.add("--eqo-w-lookahead=" + r.nextInt(randomBound)); + args.add("--eqo-w-expectedSize=" + r.nextInt(randomBound)); + + args.add("--eqo-wp-lookahead=" + r.nextInt(randomBound)); + args.add("--eqo-wp-expectedSize=" + r.nextInt(randomBound)); + + final Options options = Util.parseOptions(cmd, args.toArray(new String[0])); + + // Mock constructors and spy on calls + final AcceptorNullOracle oracle = new AcceptorNullOracle(); + + final List kWaySArgs = new ArrayList<>(); + final List kWayTArgs = new ArrayList<>(); + final List randomArgs = new ArrayList<>(); + final List randomWArgs = new ArrayList<>(); + final List randomWpArgs = new ArrayList<>(); + final List wArgs = new ArrayList<>(); + final List wpArgs = new ArrayList<>(); + + try (MockedConstruction kWayS = Mockito.mockConstruction(KWayStateCoverEQOracle.class, + (mock, context) -> kWaySArgs.addAll(context.arguments())); + MockedConstruction kWayT = Mockito.mockConstruction(KWayTransitionCoverEQOracle.class, + (mock, context) -> kWayTArgs.addAll(context.arguments())); + MockedConstruction random = Mockito.mockConstruction(RandomWordsEQOracle.class, + (mock, context) -> randomArgs.addAll(context.arguments())); + MockedConstruction randomW = Mockito.mockConstruction(RandomWMethodEQOracle.class, + (mock, context) -> randomWArgs.addAll(context.arguments())); + MockedConstruction randomWp = Mockito.mockConstruction(RandomWpMethodEQOracle.class, + (mock, context) -> randomWpArgs.addAll(context.arguments())); + MockedConstruction sample = Mockito.mockConstruction(SampleSetEQOracle.class); + MockedConstruction w = Mockito.mockConstruction(WMethodEQOracle.class, + (mock, context) -> wArgs.addAll(context.arguments())); + MockedConstruction wp = Mockito.mockConstruction(WpMethodEQOracle.class, + (mock, context) -> wpArgs.addAll(context.arguments()))) { + + EQOFactory.getRegularOracles(options, oracle); + + Assert.assertEquals(kWayS.constructed().size(), 1); + Assert.assertEquals(kWayT.constructed().size(), 1); + Assert.assertEquals(random.constructed().size(), 1); + Assert.assertEquals(randomW.constructed().size(), 1); + Assert.assertEquals(randomWp.constructed().size(), 1); + Assert.assertEquals(sample.constructed().size(), 1); + Assert.assertEquals(w.constructed().size(), 1); + Assert.assertEquals(wp.constructed().size(), 1); + } + + // Validate + final int batchSize = 2 * EQOFactory.BATCH_SIZE; + + // KWayStateCoverEQOracle + Assert.assertEquals(kWaySArgs.size(), 6); + Assert.assertSame(kWaySArgs.get(0), oracle); + // compare seeds by sampling a value from the random object + Assert.assertEquals(((Random) kWaySArgs.get(1)).nextInt(randomBound), + new Random(options.eqoParams.kWayState.seed).nextInt(randomBound)); + Assert.assertEquals(kWaySArgs.get(2), options.eqoParams.kWayState.randomWalkLen); + Assert.assertEquals(kWaySArgs.get(3), options.eqoParams.kWayState.k); + Assert.assertEquals(kWaySArgs.get(4), options.eqoParams.kWayState.combinationMethod); + Assert.assertEquals(kWaySArgs.get(5), batchSize); + + // KWayTransitionCoverEQOracle + Assert.assertEquals(kWayTArgs.size(), 10); + Assert.assertSame(kWayTArgs.get(0), oracle); + // compare seeds by sampling a value from the random object + Assert.assertEquals(((Random) kWayTArgs.get(1)).nextInt(randomBound), + new Random(options.eqoParams.kWayTransition.seed).nextInt(randomBound)); + Assert.assertEquals(kWayTArgs.get(2), options.eqoParams.kWayTransition.randomWalkLen); + Assert.assertEquals(kWayTArgs.get(3), options.eqoParams.kWayTransition.numGeneratePaths); + Assert.assertEquals(kWayTArgs.get(4), options.eqoParams.kWayTransition.maxPathLen); + Assert.assertEquals(kWayTArgs.get(5), options.eqoParams.kWayTransition.maxNumberOfSteps); + Assert.assertEquals(kWayTArgs.get(6), options.eqoParams.kWayTransition.k); + Assert.assertEquals(kWayTArgs.get(7), options.eqoParams.kWayTransition.optimizationMetric); + Assert.assertEquals(kWayTArgs.get(8), options.eqoParams.kWayTransition.generationMethod); + Assert.assertEquals(kWayTArgs.get(9), batchSize); + + // RandomWordsEQOracle + Assert.assertEquals(randomArgs.size(), 6); + Assert.assertSame(randomArgs.get(0), oracle); + Assert.assertEquals(randomArgs.get(1), options.eqoParams.random.minLength); + Assert.assertEquals(randomArgs.get(2), options.eqoParams.random.maxLength); + Assert.assertEquals(randomArgs.get(3), options.eqoParams.random.maxTests); + // compare seeds by sampling a value from the random object + Assert.assertEquals(((Random) randomArgs.get(4)).nextInt(randomBound), + new Random(options.eqoParams.random.seed).nextInt(randomBound)); + Assert.assertEquals(randomArgs.get(5), batchSize); + + // RandomWMethodEQOracle + Assert.assertEquals(randomWArgs.size(), 6); + Assert.assertSame(randomWArgs.get(0), oracle); + Assert.assertEquals(randomWArgs.get(1), options.eqoParams.randomWMethod.minimalSize); + Assert.assertEquals(randomWArgs.get(2), options.eqoParams.randomWMethod.rndLength); + Assert.assertEquals(randomWArgs.get(3), options.eqoParams.randomWMethod.bound); + // compare seeds by sampling a value from the random object + Assert.assertEquals(((Random) randomWArgs.get(4)).nextInt(randomBound), + new Random(options.eqoParams.randomWMethod.seed).nextInt(randomBound)); + Assert.assertEquals(randomWArgs.get(5), batchSize); + + // RandomWpMethodEQOracle + Assert.assertEquals(randomWpArgs.size(), 6); + Assert.assertSame(randomWArgs.get(0), oracle); + Assert.assertEquals(randomWpArgs.get(1), options.eqoParams.randomWpMethod.minimalSize); + Assert.assertEquals(randomWpArgs.get(2), options.eqoParams.randomWpMethod.rndLength); + Assert.assertEquals(randomWpArgs.get(3), options.eqoParams.randomWpMethod.bound); + // compare seeds by sampling a value from the random object + Assert.assertEquals(((Random) randomWpArgs.get(4)).nextInt(randomBound), + new Random(options.eqoParams.randomWpMethod.seed).nextInt(randomBound)); + Assert.assertEquals(randomWpArgs.get(5), batchSize); + + // WMethodEQOracle + Assert.assertEquals(wArgs.size(), 4); + Assert.assertSame(wArgs.get(0), oracle); + Assert.assertEquals(wArgs.get(1), options.eqoParams.wMethod.lookahead); + Assert.assertEquals(wArgs.get(2), options.eqoParams.wMethod.expectedSize); + Assert.assertEquals(wArgs.get(3), batchSize); + + // WpMethodEQOracle + Assert.assertEquals(wpArgs.size(), 4); + Assert.assertSame(wpArgs.get(0), oracle); + Assert.assertEquals(wpArgs.get(1), options.eqoParams.wpMethod.lookahead); + Assert.assertEquals(wpArgs.get(2), options.eqoParams.wpMethod.expectedSize); + Assert.assertEquals(wpArgs.get(3), batchSize); + } + + @Test + public void testSBAParameters() { + + // Set up randomized options + final Random r = new Random(42); + final int randomBound = 1_000; + final List args = new ArrayList<>(); + + args.add(ApplicationIT.STATEFUL); + args.add(ApplicationIT.STATEFUL); + args.add("-sa"); + Stream.of(EQOracle.RANDOM, EQOracle.SAMPLE, EQOracle.W).map(EQOracle::name).forEach(n -> args.add("-e" + n)); + + args.add("--eqo-random-minLength=" + r.nextInt(randomBound)); + args.add("--eqo-random-maxLength=" + r.nextInt(randomBound)); + args.add("--eqo-random-maxTests=" + r.nextInt(randomBound)); + args.add("--eqo-random-seed=" + r.nextInt(randomBound)); + + args.add("--eqo-w-lookahead=" + r.nextInt(randomBound)); + args.add("--eqo-w-expectedSize=" + r.nextInt(randomBound)); + + final Options options = Util.parseOptions(cmd, args.toArray(new String[0])); + + // Mock constructors and spy on calls + final AcceptorNullOracle oracle = new AcceptorNullOracle(); + + final List randomArgs = new ArrayList<>(); + final List wArgs = new ArrayList<>(); + + try (MockedConstruction random = Mockito.mockConstruction(RandomWellMatchedWordsEQOracle.class, + (mock, context) -> randomArgs.addAll(context.arguments())); + MockedConstruction sample = Mockito.mockConstruction(SampleSetEQOracle.class); + MockedConstruction w = Mockito.mockConstruction(de.learnlib.oracle.equivalence.sba.WMethodEQOracle.class, + (mock, context) -> wArgs.addAll(context.arguments()))) { + + EQOFactory.getSBAOracles(options, oracle); + + Assert.assertEquals(random.constructed().size(), 1); + Assert.assertEquals(sample.constructed().size(), 1); + Assert.assertEquals(w.constructed().size(), 1); + } + + // Validate + final int batchSize = 2 * EQOFactory.BATCH_SIZE; + + // RandomWellMatchedWordsEQOracle + Assert.assertEquals(randomArgs.size(), 7); + // compare seeds by sampling a value from the random object + Assert.assertEquals(((Random) randomArgs.get(0)).nextInt(randomBound), + new Random(options.eqoParams.random.seed).nextInt(randomBound)); + Assert.assertSame(randomArgs.get(1), oracle); + Assert.assertEquals(randomArgs.get(2), EQOFactory.RANDOM_CALL_PROB); + Assert.assertEquals(randomArgs.get(3), options.eqoParams.random.maxTests); + Assert.assertEquals(randomArgs.get(4), options.eqoParams.random.minLength); + Assert.assertEquals(randomArgs.get(5), options.eqoParams.random.maxLength); + Assert.assertEquals(randomArgs.get(6), batchSize); + + // WMethodEQOracle + Assert.assertEquals(wArgs.size(), 4); + Assert.assertSame(wArgs.get(0), oracle); + Assert.assertEquals(wArgs.get(1), options.eqoParams.wMethod.lookahead); + Assert.assertEquals(wArgs.get(2), options.eqoParams.wMethod.expectedSize); + Assert.assertEquals(wArgs.get(3), batchSize); + } + + @Test + public void testSPAParameters() { + + // Set up randomized options + final Random r = new Random(42); + final int randomBound = 1_000; + final List args = new ArrayList<>(); + + args.add(ApplicationIT.STATEFUL); + args.add(ApplicationIT.STATEFUL); + args.add("-sa"); + Stream.of(EQOracle.RANDOM, EQOracle.SAMPLE, EQOracle.W, EQOracle.WP) + .map(EQOracle::name) + .forEach(n -> args.add("-e" + n)); + + args.add("--eqo-random-minLength=" + r.nextInt(randomBound)); + args.add("--eqo-random-maxLength=" + r.nextInt(randomBound)); + args.add("--eqo-random-maxTests=" + r.nextInt(randomBound)); + args.add("--eqo-random-seed=" + r.nextInt(randomBound)); + + args.add("--eqo-w-lookahead=" + r.nextInt(randomBound)); + args.add("--eqo-w-expectedSize=" + r.nextInt(randomBound)); + + args.add("--eqo-wp-lookahead=" + r.nextInt(randomBound)); + args.add("--eqo-wp-expectedSize=" + r.nextInt(randomBound)); + + final Options options = Util.parseOptions(cmd, args.toArray(new String[0])); + + // Mock constructors and spy on calls + final AcceptorNullOracle oracle = new AcceptorNullOracle(); + + final List randomArgs = new ArrayList<>(); + final List wArgs = new ArrayList<>(); + final List wpArgs = new ArrayList<>(); + + try (MockedConstruction random = Mockito.mockConstruction(RandomWellMatchedWordsEQOracle.class, + (mock, context) -> randomArgs.addAll(context.arguments())); + MockedConstruction sample = Mockito.mockConstruction(SampleSetEQOracle.class); + MockedConstruction w = Mockito.mockConstruction(de.learnlib.oracle.equivalence.spa.WMethodEQOracle.class, + (mock, context) -> wArgs.addAll(context.arguments())); + MockedConstruction wp = Mockito.mockConstruction(de.learnlib.oracle.equivalence.spa.WpMethodEQOracle.class, + (mock, context) -> wpArgs.addAll(context.arguments()))) { + + EQOFactory.getSPAOracles(options, oracle); + + Assert.assertEquals(random.constructed().size(), 1); + Assert.assertEquals(sample.constructed().size(), 1); + Assert.assertEquals(w.constructed().size(), 1); + Assert.assertEquals(wp.constructed().size(), 1); + } + + // Validate + final int batchSize = 2 * EQOFactory.BATCH_SIZE; + + // RandomWellMatchedWordsEQOracle + Assert.assertEquals(randomArgs.size(), 7); + // compare seeds by sampling a value from the random object + Assert.assertEquals(((Random) randomArgs.get(0)).nextInt(randomBound), + new Random(options.eqoParams.random.seed).nextInt(randomBound)); + Assert.assertSame(randomArgs.get(1), oracle); + Assert.assertEquals(randomArgs.get(2), EQOFactory.RANDOM_CALL_PROB); + Assert.assertEquals(randomArgs.get(3), options.eqoParams.random.maxTests); + Assert.assertEquals(randomArgs.get(4), options.eqoParams.random.minLength); + Assert.assertEquals(randomArgs.get(5), options.eqoParams.random.maxLength); + Assert.assertEquals(randomArgs.get(6), batchSize); + + // WMethodEQOracle + Assert.assertEquals(wArgs.size(), 4); + Assert.assertSame(wArgs.get(0), oracle); + Assert.assertEquals(wArgs.get(1), options.eqoParams.wMethod.lookahead); + Assert.assertEquals(wArgs.get(2), options.eqoParams.wMethod.expectedSize); + Assert.assertEquals(wArgs.get(3), batchSize); + + // WpMethodEQOracle + Assert.assertEquals(wpArgs.size(), 4); + Assert.assertSame(wpArgs.get(0), oracle); + Assert.assertEquals(wpArgs.get(1), options.eqoParams.wpMethod.lookahead); + Assert.assertEquals(wpArgs.get(2), options.eqoParams.wpMethod.expectedSize); + Assert.assertEquals(wpArgs.get(3), batchSize); + } + + @Test + public void testSPMMParameters() { + // Set up randomized options + final Random r = new Random(42); + final int randomBound = 1_000; + final List args = new ArrayList<>(); + + args.add(ApplicationIT.STATEFUL); + args.add(ApplicationIT.STATEFUL); + args.add("-sa"); + Stream.of(EQOracle.RANDOM, EQOracle.SAMPLE, EQOracle.W).map(EQOracle::name).forEach(n -> args.add("-e" + n)); + + args.add("--eqo-random-minLength=" + r.nextInt(randomBound)); + args.add("--eqo-random-maxLength=" + r.nextInt(randomBound)); + args.add("--eqo-random-maxTests=" + r.nextInt(randomBound)); + args.add("--eqo-random-seed=" + r.nextInt(randomBound)); + + args.add("--eqo-w-lookahead=" + r.nextInt(randomBound)); + args.add("--eqo-w-expectedSize=" + r.nextInt(randomBound)); + + final Options options = Util.parseOptions(cmd, args.toArray(new String[0])); + + // Mock constructors and spy on calls + final TransducerNullOracle oracle = new TransducerNullOracle(); + + final List randomArgs = new ArrayList<>(); + final List wArgs = new ArrayList<>(); + + try (MockedConstruction random = Mockito.mockConstruction(RandomWellMatchedWordsEQOracle.class, + (mock, context) -> randomArgs.addAll(context.arguments())); + MockedConstruction sample = Mockito.mockConstruction(SampleSetEQOracle.class); + MockedConstruction w = Mockito.mockConstruction(de.learnlib.oracle.equivalence.spmm.WMethodEQOracle.class, + (mock, context) -> wArgs.addAll(context.arguments()))) { + + EQOFactory.getSPMMOracles(options, oracle); + + Assert.assertEquals(random.constructed().size(), 1); + Assert.assertEquals(sample.constructed().size(), 1); + Assert.assertEquals(w.constructed().size(), 1); + } + + // Validate + final int batchSize = 2 * EQOFactory.BATCH_SIZE; + + // RandomWellMatchedWordsEQOracle + Assert.assertEquals(randomArgs.size(), 7); + // compare seeds by sampling a value from the random object + Assert.assertEquals(((Random) randomArgs.get(0)).nextInt(randomBound), + new Random(options.eqoParams.random.seed).nextInt(randomBound)); + Assert.assertSame(randomArgs.get(1), oracle); + Assert.assertEquals(randomArgs.get(2), EQOFactory.RANDOM_CALL_PROB); + Assert.assertEquals(randomArgs.get(3), options.eqoParams.random.maxTests); + Assert.assertEquals(randomArgs.get(4), options.eqoParams.random.minLength); + Assert.assertEquals(randomArgs.get(5), options.eqoParams.random.maxLength); + Assert.assertEquals(randomArgs.get(6), batchSize); + + // WMethodEQOracle + Assert.assertEquals(wArgs.size(), 4); + Assert.assertSame(wArgs.get(0), oracle); + Assert.assertEquals(wArgs.get(1), options.eqoParams.wMethod.lookahead); + Assert.assertEquals(wArgs.get(2), options.eqoParams.wMethod.expectedSize); + Assert.assertEquals(wArgs.get(3), batchSize); + } + + private Options buildOptions(Set remaining, EQOracle... oracles) { + + final String[] args = new String[oracles.length + 2]; + + args[0] = ApplicationIT.STATELESS; + args[1] = "-sa"; + + for (int i = 0; i < oracles.length; i++) { + EQOracle oracle = oracles[i]; + remaining.remove(oracle); + args[i + 2] = "-e" + oracle.name(); + } + + return Util.parseOptions(cmd, args); + } +} diff --git a/cli/src/test/java/de/learnlib/cli/factory/LearnerFactoryTest.java b/cli/src/test/java/de/learnlib/cli/factory/LearnerFactoryTest.java new file mode 100644 index 000000000..2a4f69ea5 --- /dev/null +++ b/cli/src/test/java/de/learnlib/cli/factory/LearnerFactoryTest.java @@ -0,0 +1,560 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.factory; + +import java.io.OutputStream; +import java.io.PrintWriter; + +import de.learnlib.algorithm.LearningAlgorithm; +import de.learnlib.algorithm.LearningAlgorithm.DFALearner; +import de.learnlib.algorithm.LearningAlgorithm.MealyLearner; +import de.learnlib.algorithm.adt.learner.ADTLearner; +import de.learnlib.algorithm.dhc.mealy.MealyDHC; +import de.learnlib.algorithm.kv.dfa.KearnsVaziraniDFA; +import de.learnlib.algorithm.kv.mealy.KearnsVaziraniMealy; +import de.learnlib.algorithm.lambda.lstar.LLambdaDFA; +import de.learnlib.algorithm.lambda.lstar.LLambdaMealy; +import de.learnlib.algorithm.lambda.ttt.dfa.TTTLambdaDFA; +import de.learnlib.algorithm.lambda.ttt.mealy.TTTLambdaMealy; +import de.learnlib.algorithm.lsharp.LSharpMealy; +import de.learnlib.algorithm.lstar.dfa.ExtensibleLStarDFA; +import de.learnlib.algorithm.lstar.mealy.ExtensibleLStarMealy; +import de.learnlib.algorithm.malerpnueli.MalerPnueliDFA; +import de.learnlib.algorithm.malerpnueli.MalerPnueliMealy; +import de.learnlib.algorithm.nlstar.NLStarLearner; +import de.learnlib.algorithm.observationpack.dfa.OPLearnerDFA; +import de.learnlib.algorithm.observationpack.mealy.OPLearnerMealy; +import de.learnlib.algorithm.observationpack.vpa.OPLearnerVPA; +import de.learnlib.algorithm.procedural.sba.SBALearner; +import de.learnlib.algorithm.procedural.spa.SPALearner; +import de.learnlib.algorithm.procedural.spmm.SPMMLearner; +import de.learnlib.algorithm.rivestschapire.RivestSchapireDFA; +import de.learnlib.algorithm.rivestschapire.RivestSchapireMealy; +import de.learnlib.algorithm.sparse.SparseLearner; +import de.learnlib.algorithm.ttt.dfa.TTTLearnerDFA; +import de.learnlib.algorithm.ttt.mealy.TTTLearnerMealy; +import de.learnlib.algorithm.ttt.vpa.TTTLearnerVPA; +import de.learnlib.cli.Application; +import de.learnlib.cli.ApplicationIT; +import de.learnlib.cli.option.Learner; +import de.learnlib.cli.option.Options; +import de.learnlib.cli.util.AcceptorNullOracle; +import de.learnlib.cli.util.AdaptiveNullOracle; +import de.learnlib.cli.util.Constructor.AdaptiveConstructor; +import de.learnlib.cli.util.Constructor.DFAConstructor; +import de.learnlib.cli.util.Constructor.MealyConstructor; +import de.learnlib.cli.util.Constructor.PresetConstructor; +import de.learnlib.cli.util.TransducerNullOracle; +import de.learnlib.cli.util.Util; +import net.automatalib.alphabet.Alphabet; +import net.automatalib.alphabet.ProceduralInputAlphabet; +import net.automatalib.alphabet.VPAlphabet; +import net.automatalib.alphabet.impl.Alphabets; +import net.automatalib.alphabet.impl.DefaultProceduralInputAlphabet; +import net.automatalib.alphabet.impl.DefaultVPAlphabet; +import net.automatalib.automaton.fsa.NFA; +import net.automatalib.automaton.procedural.SBA; +import net.automatalib.automaton.procedural.SPA; +import net.automatalib.automaton.procedural.SPMM; +import net.automatalib.automaton.transducer.MealyMachine; +import net.automatalib.automaton.vpa.OneSEVPA; +import net.automatalib.word.Word; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import picocli.CommandLine; + +public class LearnerFactoryTest { + + private final CommandLine cmd; + + public LearnerFactoryTest() { + cmd = new CommandLine(new Application()); + cmd.setErr(new PrintWriter(OutputStream.nullOutputStream())); + } + + @DataProvider(name = "dfa") + private static Object[][] dfaConfigs() { + final Object[][] result = new Object[Learner.values().length][]; + + // use for-each loop + switch case to make compiler check for completeness + for (Learner value : Learner.values()) { + result[value.ordinal()] = switch (value) { + case ADT -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-lADT"}, Exception.class}; + case DHC -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-lDHC"}, Exception.class}; + case KEARNS_VAZIRANI -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-lKEARNS_VAZIRANI"}, + KearnsVaziraniDFA.class}; + case L_LAMBDA -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-lL_LAMBDA"}, LLambdaDFA.class}; + case L_SHARP -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-lL_SHARP"}, Exception.class}; + case L_STAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-lL_STAR"}, + ExtensibleLStarDFA.class}; + case MALER_PNUELI -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-lMALER_PNUELI"}, + MalerPnueliDFA.class}; + case NL_STAR -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-lNL_STAR"}, Exception.class}; + case OBSERVATION_PACK -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-lOBSERVATION_PACK"}, + OPLearnerDFA.class}; + case RIVEST_SCHAPIRE -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-lRIVEST_SCHAPIRE"}, + RivestSchapireDFA.class}; + case SPARSE -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-lSPARSE"}, Exception.class}; + case TTT -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-lTTT"}, TTTLearnerDFA.class}; + case TTT_LAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-lTTT_LAMBDA"}, + TTTLambdaDFA.class}; + }; + } + + return result; + } + + @Test(dataProvider = "dfa") + public void testDFALearners(String[] args, Class clazz) { + final Options options = Util.parseOptions(cmd, args); + + if (Exception.class.isAssignableFrom(clazz)) { + Assert.assertThrows(() -> LearnerFactory.getDFALearner(options)); + } else { + final DFAConstructor, String> dfaConstructor = LearnerFactory.getDFALearner(options); + final DFALearner learner = + dfaConstructor.constructLearner(Alphabets.fromArray(), new AcceptorNullOracle()); + Assert.assertTrue(clazz.isInstance(learner)); + } + } + + @DataProvider(name = "mealy") + private static Object[][] mealyConfigs() { + final Object[][] result = new Object[Learner.values().length][]; + + // use for-each loop + switch case to make compiler check for completeness + for (Learner value : Learner.values()) { + result[value.ordinal()] = switch (value) { + case ADT -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lADT"}, + Exception.class}; + case DHC -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lDHC"}, + MealyDHC.class}; + case KEARNS_VAZIRANI -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lKEARNS_VAZIRANI"}, + KearnsVaziraniMealy.class}; + case L_LAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lL_LAMBDA"}, + LLambdaMealy.class}; + case L_SHARP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lL_SHARP"}, + Exception.class}; + case L_STAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lL_STAR"}, + ExtensibleLStarMealy.class}; + case MALER_PNUELI -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lMALER_PNUELI"}, + MalerPnueliMealy.class}; + case NL_STAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lNL_STAR"}, + Exception.class}; + case OBSERVATION_PACK -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lOBSERVATION_PACK"}, + OPLearnerMealy.class}; + case RIVEST_SCHAPIRE -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lRIVEST_SCHAPIRE"}, + RivestSchapireMealy.class}; + case SPARSE -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lSPARSE"}, + SparseLearner.class}; + case TTT -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lTTT"}, + TTTLearnerMealy.class}; + case TTT_LAMBDA -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lTTT_LAMBDA"}, + TTTLambdaMealy.class}; + }; + } + + return result; + } + + @Test(dataProvider = "mealy") + public void testMealyLearners(String[] args, Class clazz) { + final Options options = Util.parseOptions(cmd, args); + + if (Exception.class.isAssignableFrom(clazz)) { + Assert.assertThrows(() -> LearnerFactory.getMealyLearner(options)); + } else { + final MealyConstructor, String, String> mealyConstructor = + LearnerFactory.getMealyLearner(options); + final MealyLearner learner = + mealyConstructor.constructLearner(Alphabets.fromArray(), new TransducerNullOracle()); + Assert.assertTrue(clazz.isInstance(learner)); + } + } + + @DataProvider(name = "adaptive") + private static Object[][] adaptiveConfigs() { + final Object[][] result = new Object[Learner.values().length][]; + + // use for-each loop + switch case to make compiler check for completeness + for (Learner value : Learner.values()) { + result[value.ordinal()] = switch (value) { + case ADT -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lADT"}, + ADTLearner.class}; + case DHC -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lDHC"}, + Exception.class}; + case KEARNS_VAZIRANI -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lKEARNS_VAZIRANI"}, + Exception.class}; + case L_LAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lL_LAMBDA"}, + Exception.class}; + case L_SHARP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lL_SHARP"}, + LSharpMealy.class}; + case L_STAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lL_STAR"}, + Exception.class}; + case MALER_PNUELI -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lMALER_PNUELI"}, + Exception.class}; + case NL_STAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lNL_STAR"}, + Exception.class}; + case OBSERVATION_PACK -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lOBSERVATION_PACK"}, + Exception.class}; + case RIVEST_SCHAPIRE -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lRIVEST_SCHAPIRE"}, + Exception.class}; + case SPARSE -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lSPARSE"}, + Exception.class}; + case TTT -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lTTT"}, + Exception.class}; + case TTT_LAMBDA -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lTTT_LAMBDA"}, + Exception.class}; + }; + } + + return result; + } + + @Test(dataProvider = "adaptive") + public void testAdaptiveLearners(String[] args, Class clazz) { + final Options options = Util.parseOptions(cmd, args); + + if (Exception.class.isAssignableFrom(clazz)) { + Assert.assertThrows(() -> LearnerFactory.getAdaptiveLearner(options)); + } else { + final AdaptiveConstructor, MealyMachine, String, String> + adaptiveConstructor = LearnerFactory.getAdaptiveLearner(options); + final LearningAlgorithm, String, Word> learner = + adaptiveConstructor.constructLearner(Alphabets.fromArray(), new AdaptiveNullOracle()); + Assert.assertTrue(clazz.isInstance(learner)); + } + } + + @DataProvider(name = "nfa") + private static Object[][] nfaConfigs() { + final Object[][] result = new Object[Learner.values().length][]; + + // use for-each loop + switch case to make compiler check for completeness + for (Learner value : Learner.values()) { + result[value.ordinal()] = switch (value) { + case ADT -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lADT"}, Exception.class}; + case DHC -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lDHC"}, Exception.class}; + case KEARNS_VAZIRANI -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lKEARNS_VAZIRANI"}, + Exception.class}; + case L_LAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lL_LAMBDA"}, + Exception.class}; + case L_SHARP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lL_SHARP"}, + Exception.class}; + case L_STAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lL_STAR"}, + Exception.class}; + case MALER_PNUELI -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lMALER_PNUELI"}, + Exception.class}; + case NL_STAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lNL_STAR"}, + NLStarLearner.class}; + case OBSERVATION_PACK -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lOBSERVATION_PACK"}, + Exception.class}; + case RIVEST_SCHAPIRE -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lRIVEST_SCHAPIRE"}, + Exception.class}; + case SPARSE -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lSPARSE"}, + Exception.class}; + case TTT -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lTTT"}, Exception.class}; + case TTT_LAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lTTT_LAMBDA"}, + Exception.class}; + }; + } + + return result; + } + + @Test(dataProvider = "nfa") + public void testNFALearners(String[] args, Class clazz) { + final Options options = Util.parseOptions(cmd, args); + + if (Exception.class.isAssignableFrom(clazz)) { + Assert.assertThrows(() -> LearnerFactory.getNFALearner(options)); + } else { + final PresetConstructor, NFA, String, Boolean> nfaConstructor = + LearnerFactory.getNFALearner(options); + final LearningAlgorithm, String, Boolean> learner = + nfaConstructor.constructLearner(Alphabets.fromArray(), new AcceptorNullOracle()); + Assert.assertTrue(clazz.isInstance(learner)); + } + } + + @DataProvider(name = "sba") + private static Object[][] sbaConfigs() { + final Object[][] result = new Object[Learner.values().length][]; + + // use for-each loop + switch case to make compiler check for completeness + for (Learner value : Learner.values()) { + result[value.ordinal()] = switch (value) { + case ADT -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSBA", "-lADT"}, Exception.class}; + case DHC -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSBA", "-lDHC"}, Exception.class}; + case KEARNS_VAZIRANI -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSBA", "-lKEARNS_VAZIRANI"}, + SBALearner.class}; + case L_LAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSBA", "-lL_LAMBDA"}, + SBALearner.class}; + case L_SHARP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSBA", "-lL_SHARP"}, + Exception.class}; + case L_STAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSBA", "-lL_STAR"}, + SBALearner.class}; + case MALER_PNUELI -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSBA", "-lMALER_PNUELI"}, + SBALearner.class}; + case NL_STAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSBA", "-lNL_STAR"}, + Exception.class}; + case OBSERVATION_PACK -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSBA", "-lOBSERVATION_PACK"}, + SBALearner.class}; + case RIVEST_SCHAPIRE -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSBA", "-lRIVEST_SCHAPIRE"}, + SBALearner.class}; + case SPARSE -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSBA", "-lSPARSE"}, + Exception.class}; + case TTT -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSBA", "-lTTT"}, + SBALearner.class}; + case TTT_LAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSBA", "-lTTT_LAMBDA"}, + SBALearner.class}; + }; + } + + return result; + } + + @Test(dataProvider = "sba") + public void testSBALearners(String[] args, Class clazz) { + final Options options = Util.parseOptions(cmd, args); + + if (Exception.class.isAssignableFrom(clazz)) { + Assert.assertThrows(() -> LearnerFactory.getSBALearner(options)); + } else { + final PresetConstructor, SBA, String, Boolean> sbaConstructor = + LearnerFactory.getSBALearner(options); + final LearningAlgorithm, String, Boolean> learner = + sbaConstructor.constructLearner(new DefaultProceduralInputAlphabet<>(Alphabets.fromArray(), + Alphabets.fromArray(), + ""), new AcceptorNullOracle()); + Assert.assertTrue(clazz.isInstance(learner)); + } + } + + @DataProvider(name = "spa") + private static Object[][] spaConfigs() { + final Object[][] result = new Object[Learner.values().length][]; + + // use for-each loop + switch case to make compiler check for completeness + for (Learner value : Learner.values()) { + result[value.ordinal()] = switch (value) { + case ADT -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPA", "-lADT"}, Exception.class}; + case DHC -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPA", "-lDHC"}, Exception.class}; + case KEARNS_VAZIRANI -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPA", "-lKEARNS_VAZIRANI"}, + SPALearner.class}; + case L_LAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPA", "-lL_LAMBDA"}, + SPALearner.class}; + case L_SHARP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPA", "-lL_SHARP"}, + Exception.class}; + case L_STAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPA", "-lL_STAR"}, + SPALearner.class}; + case MALER_PNUELI -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPA", "-lMALER_PNUELI"}, + SPALearner.class}; + case NL_STAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPA", "-lNL_STAR"}, + Exception.class}; + case OBSERVATION_PACK -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPA", "-lOBSERVATION_PACK"}, + SPALearner.class}; + case RIVEST_SCHAPIRE -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPA", "-lRIVEST_SCHAPIRE"}, + SPALearner.class}; + case SPARSE -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPA", "-lSPARSE"}, + Exception.class}; + case TTT -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPA", "-lTTT"}, + SPALearner.class}; + case TTT_LAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPA", "-lTTT_LAMBDA"}, + SPALearner.class}; + }; + } + + return result; + } + + @Test(dataProvider = "spa") + public void testSPALearners(String[] args, Class clazz) { + final Options options = Util.parseOptions(cmd, args); + + if (Exception.class.isAssignableFrom(clazz)) { + Assert.assertThrows(() -> LearnerFactory.getSBALearner(options)); + } else { + final PresetConstructor, SPA, String, Boolean> spaConstructor = + LearnerFactory.getSPALearner(options); + final LearningAlgorithm, String, Boolean> learner = + spaConstructor.constructLearner(new DefaultProceduralInputAlphabet<>(Alphabets.fromArray(), + Alphabets.fromArray(), + ""), new AcceptorNullOracle()); + Assert.assertTrue(clazz.isInstance(learner)); + } + } + + @DataProvider(name = "spmm") + private static Object[][] spmmConfigs() { + final Object[][] result = new Object[Learner.values().length][]; + + // use for-each loop + switch case to make compiler check for completeness + for (Learner value : Learner.values()) { + result[value.ordinal()] = switch (value) { + case ADT -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lADT"}, + Exception.class}; + case DHC -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lDHC"}, + SPMMLearner.class}; + case KEARNS_VAZIRANI -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lKEARNS_VAZIRANI"}, + SPMMLearner.class}; + case L_LAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lL_LAMBDA"}, + SPMMLearner.class}; + case L_SHARP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lL_SHARP"}, + Exception.class}; + case L_STAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lL_STAR"}, + SPMMLearner.class}; + case MALER_PNUELI -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lMALER_PNUELI"}, + SPMMLearner.class}; + case NL_STAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lNL_STAR"}, + Exception.class}; + case OBSERVATION_PACK -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lOBSERVATION_PACK"}, + SPMMLearner.class}; + case RIVEST_SCHAPIRE -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lRIVEST_SCHAPIRE"}, + SPMMLearner.class}; + case SPARSE -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lSPARSE"}, + SPMMLearner.class}; + case TTT -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lTTT"}, + SPMMLearner.class}; + case TTT_LAMBDA -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lTTT_LAMBDA"}, + SPMMLearner.class}; + }; + } + + return result; + } + + @Test(dataProvider = "spmm") + public void testSPMMLearners(String[] args, Class clazz) { + final Options options = Util.parseOptions(cmd, args); + + if (Exception.class.isAssignableFrom(clazz)) { + Assert.assertThrows(() -> LearnerFactory.getSBALearner(options)); + } else { + final PresetConstructor, SPMM, String, Word> + spmmConstructor = LearnerFactory.getSPMMLearner(options); + final LearningAlgorithm, String, Word> learner = + spmmConstructor.constructLearner(new DefaultProceduralInputAlphabet<>(Alphabets.fromArray(), + Alphabets.fromArray(), + ""), + new TransducerNullOracle()); + Assert.assertTrue(clazz.isInstance(learner)); + } + } + + @DataProvider(name = "vpa") + private static Object[][] vpaConfigs() { + final Object[][] result = new Object[Learner.values().length][]; + + // use for-each loop + switch case to make compiler check for completeness + for (Learner value : Learner.values()) { + result[value.ordinal()] = switch (value) { + case ADT -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lADT"}, Exception.class}; + case DHC -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lDHC"}, Exception.class}; + case KEARNS_VAZIRANI -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lKEARNS_VAZIRANI"}, + Exception.class}; + case L_LAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lL_LAMBDA"}, + Exception.class}; + case L_SHARP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lL_SHARP"}, + Exception.class}; + case L_STAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lL_STAR"}, + Exception.class}; + case MALER_PNUELI -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lMALER_PNUELI"}, + Exception.class}; + case NL_STAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lNL_STAR"}, + Exception.class}; + case OBSERVATION_PACK -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lOBSERVATION_PACK"}, + OPLearnerVPA.class}; + case RIVEST_SCHAPIRE -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lRIVEST_SCHAPIRE"}, + Exception.class}; + case SPARSE -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lSPARSE"}, + Exception.class}; + case TTT -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lTTT"}, + TTTLearnerVPA.class}; + case TTT_LAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lTTT_LAMBDA"}, + Exception.class}; + }; + } + + return result; + } + + @Test(dataProvider = "vpa") + public void testVPALearners(String[] args, Class clazz) { + final Options options = Util.parseOptions(cmd, args); + + if (Exception.class.isAssignableFrom(clazz)) { + Assert.assertThrows(() -> LearnerFactory.getVPALearner(options)); + } else { + final PresetConstructor, OneSEVPA, String, Boolean> vpaConstructor = + LearnerFactory.getVPALearner(options); + final LearningAlgorithm, String, Boolean> learner = + vpaConstructor.constructLearner(new DefaultVPAlphabet<>(Alphabets.fromArray(), + Alphabets.fromArray(), + Alphabets.fromArray()), + new AcceptorNullOracle()); + Assert.assertTrue(clazz.isInstance(learner)); + } + } +} diff --git a/cli/src/test/java/de/learnlib/cli/factory/MQOFactoryTest.java b/cli/src/test/java/de/learnlib/cli/factory/MQOFactoryTest.java new file mode 100644 index 000000000..9af4acd6e --- /dev/null +++ b/cli/src/test/java/de/learnlib/cli/factory/MQOFactoryTest.java @@ -0,0 +1,330 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.factory; + +import java.io.File; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.BiFunction; + +import de.learnlib.cli.AbstractPythonTest; +import de.learnlib.cli.ApplicationIT; +import de.learnlib.cli.option.Options; +import de.learnlib.cli.util.AcceptorNullOracle; +import de.learnlib.cli.util.AdaptiveNullOracle; +import de.learnlib.cli.util.TransducerNullOracle; +import de.learnlib.filter.statistic.oracle.CounterAdaptiveQueryOracle; +import de.learnlib.filter.statistic.oracle.CounterOracle; +import de.learnlib.oracle.AdaptiveMembershipOracle; +import de.learnlib.oracle.MembershipOracle; +import de.learnlib.oracle.membership.CLIOracle; +import de.learnlib.oracle.membership.CLIOutputAdaptiveOracle; +import de.learnlib.oracle.membership.CLIOutputOracle; +import de.learnlib.oracle.membership.StdInOracle; +import de.learnlib.oracle.membership.StdInOutputAdaptiveOracle; +import de.learnlib.oracle.membership.StdInOutputOracle; +import de.learnlib.query.DefaultQuery; +import de.learnlib.statistic.Statistics; +import de.learnlib.util.mealy.PresetAdaptiveQuery; +import net.automatalib.alphabet.Alphabet; +import net.automatalib.alphabet.impl.Alphabets; +import net.automatalib.word.Word; +import org.mockito.Answers; +import org.mockito.MockedConstruction; +import org.mockito.MockedStatic; +import org.mockito.MockedStatic.Verification; +import org.mockito.Mockito; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class MQOFactoryTest extends AbstractPythonTest { + + private final File sulFile; + + public MQOFactoryTest() { + final URL resource = MQOFactoryTest.class.getResource("/sul/stateless.py"); + Assert.assertNotNull(resource); + this.sulFile = new File(resource.getFile()); + } + + @Test + public void testSingleAcceptorOracle() { + testSingleOracle(MQOFactory::buildSingleAcceptorOracle, CLIOracle.class, StdInOracle.class, 1); + } + + @Test + public void testSingleTransducerOracle() { + testSingleOracle(MQOFactory::buildSingleTransducerOracle, CLIOutputOracle.class, StdInOutputOracle.class, 2); + } + + @Test + public void testSingleAdaptiveOracle() { + testSingleOracle(MQOFactory::buildSingleAdaptiveOracle, + CLIOutputAdaptiveOracle.class, + StdInOutputAdaptiveOracle.class, + 2); + } + + private void testSingleOracle(BiFunction creator, + Class cliClass, + Class stdinClass, + int resetParamPos) { + final String reset = "test"; + + final Options options = new Options(); + options.delimiter = " "; + options.reset = reset; + + final List cliArgs = new ArrayList<>(); + + try (MockedConstruction cli = Mockito.mockConstruction(cliClass, + (mock, context) -> cliArgs.addAll(context.arguments()))) { + creator.apply(options, sulFile); + Assert.assertEquals(cli.constructed().size(), 1); + } + + Assert.assertEquals(cliArgs.size(), resetParamPos + 1); + Assert.assertEquals(cliArgs.get(0), Collections.singletonList(sulFile.getPath())); + Assert.assertEquals(cliArgs.get(resetParamPos), reset); + + final String additionalArgs = "additional"; + options.stdin = true; + options.additionalArgs = List.of(additionalArgs, additionalArgs); + + final List stdinArgs = new ArrayList<>(); + + try (MockedConstruction stdin = Mockito.mockConstruction(stdinClass, + (mock, context) -> stdinArgs.addAll(context.arguments()))) { + creator.apply(options, sulFile); + Assert.assertEquals(stdin.constructed().size(), 1); + } + + Assert.assertEquals(stdinArgs.size(), resetParamPos + 1); + Assert.assertEquals(stdinArgs.get(0), List.of(sulFile.getPath(), additionalArgs, additionalArgs)); + Assert.assertEquals(stdinArgs.get(resetParamPos), reset); + + File brokenFile = new File(sulFile.getAbsolutePath() + ".brokenSuffix"); + Assert.assertThrows(() -> MQOFactory.buildSingleAdaptiveOracle(options, brokenFile)); + options.stdin = false; + Assert.assertThrows(() -> MQOFactory.buildSingleAdaptiveOracle(options, brokenFile)); + } + + @Test + public void testAcceptorOracle() { + testOracle(() -> MQOFactory.buildSingleAcceptorOracle(Mockito.any(), Mockito.any()), + MQOFactory::getAcceptorOracle, + AcceptorNullOracle.class); + } + + @Test + public void testTransducerOracle() { + testOracle(() -> MQOFactory.buildSingleTransducerOracle(Mockito.any(), Mockito.any()), + MQOFactory::getTransducerOracle, + TransducerNullOracle.class); + } + + private > void testOracle(Verification mock, + BiFunction, OR> oracleFunction, + Class nullOracle) { + final Options options = new Options(); + options.sul = Arrays.asList(sulFile, sulFile); + + final Alphabet alphabet = Alphabets.fromArray("a", "b"); + final OR oracleMock = Mockito.spy(nullOracle); + + try (MockedStatic factoryMock = Mockito.mockStatic(MQOFactory.class, Answers.CALLS_REAL_METHODS)) { + factoryMock.when(mock).thenReturn(oracleMock); + + // basic invocation + OR oracle = oracleFunction.apply(options, alphabet); + + oracle.answerQuery(Word.fromSymbols("a", "b")); + Mockito.verify(oracleMock, Mockito.times(1)).answerQuery(Mockito.any(), Mockito.any()); + oracle.answerQuery(Word.fromSymbols("a", "b")); + Mockito.verify(oracleMock, Mockito.times(2)).answerQuery(Mockito.any(), Mockito.any()); + + Assert.assertTrue(Statistics.getService() + .getCount(CounterOracle.KEY_SYMBOL.withId(MQOFactory.SUL_KEY)) + .isEmpty()); + Assert.assertTrue(Statistics.getService() + .getCount(CounterOracle.KEY_SYMBOL.withId(MQOFactory.CACHE_KEY)) + .isEmpty()); + Statistics.getService().clear(); + + // with cache + Mockito.reset(new Object[]{oracleMock}); // make compiler happy + options.cache = true; + oracle = oracleFunction.apply(options, alphabet); + + oracle.answerQuery(Word.fromSymbols("a", "b")); + Mockito.verify(oracleMock, Mockito.times(1)).answerQuery(Mockito.any(), Mockito.any()); + oracle.answerQuery(Word.fromSymbols("a", "b")); + Mockito.verify(oracleMock, Mockito.times(1)).answerQuery(Mockito.any(), Mockito.any()); + + Assert.assertTrue(Statistics.getService() + .getCount(CounterOracle.KEY_SYMBOL.withId(MQOFactory.SUL_KEY)) + .isEmpty()); + Assert.assertTrue(Statistics.getService() + .getCount(CounterOracle.KEY_SYMBOL.withId(MQOFactory.CACHE_KEY)) + .isEmpty()); + Statistics.getService().clear(); + + // with stats + Mockito.reset(new Object[]{oracleMock}); // make compiler happy + options.cache = false; + options.statistics = true; + oracle = oracleFunction.apply(options, alphabet); + + oracle.answerQuery(Word.fromSymbols("a", "b")); + Mockito.verify(oracleMock, Mockito.times(1)).answerQuery(Mockito.any(), Mockito.any()); + oracle.answerQuery(Word.fromSymbols("a", "b")); + Mockito.verify(oracleMock, Mockito.times(2)).answerQuery(Mockito.any(), Mockito.any()); + + Assert.assertTrue(Statistics.getService() + .getCount(CounterOracle.KEY_SYMBOL.withId(MQOFactory.SUL_KEY)) + .isPresent()); + Assert.assertTrue(Statistics.getService() + .getCount(CounterOracle.KEY_SYMBOL.withId(MQOFactory.CACHE_KEY)) + .isEmpty()); + Statistics.getService().clear(); + + // with stats + cache + Mockito.reset(new Object[]{oracleMock}); // make compiler happy + options.cache = true; + oracle = oracleFunction.apply(options, alphabet); + + oracle.answerQuery(Word.fromSymbols("a", "b")); + Mockito.verify(oracleMock, Mockito.times(1)).answerQuery(Mockito.any(), Mockito.any()); + oracle.answerQuery(Word.fromSymbols("a", "b")); + Mockito.verify(oracleMock, Mockito.times(1)).answerQuery(Mockito.any(), Mockito.any()); + + Assert.assertTrue(Statistics.getService() + .getCount(CounterOracle.KEY_SYMBOL.withId(MQOFactory.SUL_KEY)) + .isPresent()); + Assert.assertTrue(Statistics.getService() + .getCount(CounterOracle.KEY_SYMBOL.withId(MQOFactory.CACHE_KEY)) + .isPresent()); + Statistics.getService().clear(); + } + } + + @Test + public void testAdaptiveOracle() { + final Options options = new Options(); + options.sul = Arrays.asList(sulFile, sulFile); + + final Alphabet alphabet = Alphabets.fromArray("a", "b"); + final AdaptiveNullOracle oracleMock = Mockito.spy(AdaptiveNullOracle.class); + + Assert.assertThrows(() -> MQOFactory.getAdaptiveOracle(options, alphabet)); // --reset required + + options.reset = "reset"; + + try (MockedStatic factoryMock = Mockito.mockStatic(MQOFactory.class, Answers.CALLS_REAL_METHODS)) { + factoryMock.when(() -> MQOFactory.buildSingleAdaptiveOracle(Mockito.any(), Mockito.any())) + .thenReturn(oracleMock); + + // basic invocation + AdaptiveMembershipOracle oracle = MQOFactory.getAdaptiveOracle(options, alphabet); + + oracle.processQuery(new PresetAdaptiveQuery<>(new DefaultQuery<>(Word.fromSymbols("a", "b")))); + Mockito.verify(oracleMock, Mockito.times(1)).processQuery(Mockito.any()); + oracle.processQuery(new PresetAdaptiveQuery<>(new DefaultQuery<>(Word.fromSymbols("a", "b")))); + Mockito.verify(oracleMock, Mockito.times(2)).processQuery(Mockito.any()); + + Assert.assertTrue(Statistics.getService() + .getCount(CounterAdaptiveQueryOracle.KEY_SYMBOL.withId(MQOFactory.SUL_KEY)) + .isEmpty()); + Assert.assertTrue(Statistics.getService() + .getCount(CounterAdaptiveQueryOracle.KEY_SYMBOL.withId(MQOFactory.CACHE_KEY)) + .isEmpty()); + Statistics.getService().clear(); + + // with cache + Mockito.reset(oracleMock); + options.cache = true; + oracle = MQOFactory.getAdaptiveOracle(options, alphabet); + + oracle.processQuery(new PresetAdaptiveQuery<>(new DefaultQuery<>(Word.fromSymbols("a", "b")))); + Mockito.verify(oracleMock, Mockito.times(1)).processQuery(Mockito.any()); + oracle.processQuery(new PresetAdaptiveQuery<>(new DefaultQuery<>(Word.fromSymbols("a", "b")))); + Mockito.verify(oracleMock, Mockito.times(1)).processQuery(Mockito.any()); + + Assert.assertTrue(Statistics.getService() + .getCount(CounterAdaptiveQueryOracle.KEY_SYMBOL.withId(MQOFactory.SUL_KEY)) + .isEmpty()); + Assert.assertTrue(Statistics.getService() + .getCount(CounterAdaptiveQueryOracle.KEY_SYMBOL.withId(MQOFactory.CACHE_KEY)) + .isEmpty()); + Statistics.getService().clear(); + + // with stats + Mockito.reset(oracleMock); + options.cache = false; + options.statistics = true; + oracle = MQOFactory.getAdaptiveOracle(options, alphabet); + + oracle.processQuery(new PresetAdaptiveQuery<>(new DefaultQuery<>(Word.fromSymbols("a", "b")))); + Mockito.verify(oracleMock, Mockito.times(1)).processQuery(Mockito.any()); + oracle.processQuery(new PresetAdaptiveQuery<>(new DefaultQuery<>(Word.fromSymbols("a", "b")))); + Mockito.verify(oracleMock, Mockito.times(2)).processQuery(Mockito.any()); + + Assert.assertTrue(Statistics.getService() + .getCount(CounterAdaptiveQueryOracle.KEY_SYMBOL.withId(MQOFactory.SUL_KEY)) + .isPresent()); + Assert.assertTrue(Statistics.getService() + .getCount(CounterAdaptiveQueryOracle.KEY_SYMBOL.withId(MQOFactory.CACHE_KEY)) + .isEmpty()); + Statistics.getService().clear(); + + // with stats + cache + Mockito.reset(oracleMock); + options.cache = true; + oracle = MQOFactory.getAdaptiveOracle(options, alphabet); + + oracle.processQuery(new PresetAdaptiveQuery<>(new DefaultQuery<>(Word.fromSymbols("a", "b")))); + Mockito.verify(oracleMock, Mockito.times(1)).processQuery(Mockito.any()); + oracle.processQuery(new PresetAdaptiveQuery<>(new DefaultQuery<>(Word.fromSymbols("a", "b")))); + Mockito.verify(oracleMock, Mockito.times(1)).processQuery(Mockito.any()); + + Assert.assertTrue(Statistics.getService() + .getCount(CounterAdaptiveQueryOracle.KEY_SYMBOL.withId(MQOFactory.SUL_KEY)) + .isPresent()); + Assert.assertTrue(Statistics.getService() + .getCount(CounterAdaptiveQueryOracle.KEY_SYMBOL.withId(MQOFactory.CACHE_KEY)) + .isPresent()); + Statistics.getService().clear(); + } + } + + @Test + public void testOutputTransformer() { + final File sulFile = new File(PROGRAM); + + final Options options = new Options(); + options.delimiter = "\\n"; + options.additionalArgs = Collections.singletonList(ApplicationIT.STATELESS_BROKEN); + + var mqo = MQOFactory.buildSingleTransducerOracle(options, sulFile); + + Assert.assertEquals(mqo.answerQuery(Word.epsilon()), Word.epsilon()); + Assert.assertThrows(() -> mqo.answerQuery(Word.fromLetter("a"))); + Assert.assertThrows(() -> mqo.answerQuery(Word.fromLetter("a"), Word.fromSymbols("a", "b"))); + } +} diff --git a/cli/src/test/java/de/learnlib/cli/factory/SerializationFactoryTest.java b/cli/src/test/java/de/learnlib/cli/factory/SerializationFactoryTest.java new file mode 100644 index 000000000..32bfce2a4 --- /dev/null +++ b/cli/src/test/java/de/learnlib/cli/factory/SerializationFactoryTest.java @@ -0,0 +1,319 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.factory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; + +import de.learnlib.cli.Application; +import de.learnlib.cli.ApplicationIT; +import de.learnlib.cli.option.Options; +import de.learnlib.cli.option.Output; +import de.learnlib.cli.util.Util; +import net.automatalib.alphabet.Alphabet; +import net.automatalib.automaton.fsa.DFA; +import net.automatalib.automaton.fsa.NFA; +import net.automatalib.automaton.fsa.impl.CompactDFA; +import net.automatalib.automaton.fsa.impl.CompactNFA; +import net.automatalib.automaton.procedural.SBA; +import net.automatalib.automaton.procedural.SPA; +import net.automatalib.automaton.procedural.SPMM; +import net.automatalib.automaton.transducer.MealyMachine; +import net.automatalib.automaton.transducer.impl.CompactMealy; +import net.automatalib.automaton.vpa.OneSEVPA; +import net.automatalib.serialization.InputModelSerializer; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import picocli.CommandLine; + +public class SerializationFactoryTest { + + private static final String[] AUT_CMD = new String[] {ApplicationIT.STATELESS, "-sa", "-fAUT"}; + private static final String[] BA_CMD = new String[] {ApplicationIT.STATELESS, "-sa", "-fBA"}; + private static final String[] DOT_CMD = new String[] {ApplicationIT.STATELESS, "-sa"}; + private static final String[] LV2_CMD = new String[] {ApplicationIT.STATELESS, "-sa", "-fLEARNLIBV2"}; + private static final String[] MATA_CMD = new String[] {ApplicationIT.STATELESS, "-sa", "-fMATA"}; + private static final String[] SAF_CMD = new String[] {ApplicationIT.STATELESS, "-sa", "-fSAF"}; + private static final String[] TAF_CMD = new String[] {ApplicationIT.STATELESS, "-sa", "-fTAF"}; + + private final CommandLine cmd; + + public SerializationFactoryTest() { + cmd = new CommandLine(new Application()); + cmd.setErr(new PrintWriter(OutputStream.nullOutputStream())); + } + + @DataProvider(name = "dfa") + private static Object[][] dfaConfigs() { + final Object[][] result = new Object[Output.values().length][]; + + // use for-each loop + switch case to make compiler check for completeness + for (Output value : Output.values()) { + result[value.ordinal()] = switch (value) { + case AUT -> new Object[] {AUT_CMD, "/ser/dfa.aut"}; + case BA -> new Object[] {BA_CMD, "/ser/dfa.ba"}; + case DOT -> new Object[] {DOT_CMD, "/ser/dfa.dot"}; + case LEARNLIBV2 -> new Object[] {LV2_CMD, "/ser/dfa.lv2"}; + case MATA -> new Object[] {MATA_CMD, "/ser/dfa.mata"}; + case SAF -> new Object[] {SAF_CMD, "/ser/dfa.saf"}; + case TAF -> new Object[] {TAF_CMD, "/ser/dfa.taf"}; + }; + } + + return result; + } + + @Test(dataProvider = "dfa") + public void testDFASerializers(String[] args, String resource) throws IOException { + final Options options = Util.parseOptions(cmd, args); + final InputModelSerializer> dfaSerializer = SerializerFactory.getDFASerializer(options); + final CompactDFA dfa = Util.getExampleDFA(); + + testSerializer(dfaSerializer, dfa, dfa.getInputAlphabet(), resource); + } + + @DataProvider(name = "mealy") + private static Object[][] mealyConfigs() { + final Object[][] result = new Object[Output.values().length][]; + + // use for-each loop + switch case to make compiler check for completeness + for (Output value : Output.values()) { + result[value.ordinal()] = switch (value) { + case AUT -> new Object[] {AUT_CMD, "/ser/mealy.aut", true}; + case BA -> new Object[] {BA_CMD, "/ser/mealy.ba", true}; + case DOT -> new Object[] {DOT_CMD, "/ser/mealy.dot", false}; + case LEARNLIBV2 -> new Object[] {LV2_CMD, "/ser/mealy.lv2", true}; + case MATA -> new Object[] {MATA_CMD, "/ser/mealy.mata", true}; + case SAF -> new Object[] {SAF_CMD, "/ser/mealy.saf", false}; + case TAF -> new Object[] {TAF_CMD, "/ser/mealy.taf", false}; + }; + } + + return result; + } + + @Test(dataProvider = "mealy") + public void testMealySerializers(String[] args, String resource, boolean shouldFail) throws IOException { + final Options options = Util.parseOptions(cmd, args); + + if (shouldFail) { + Assert.assertThrows(() -> SerializerFactory.getMealySerializer(options)); + } else { + final InputModelSerializer> mealySerializer = + SerializerFactory.getMealySerializer(options); + final CompactMealy mealy = Util.getExampleMealy(); + + testSerializer(mealySerializer, mealy, mealy.getInputAlphabet(), resource); + } + } + + @DataProvider(name = "nfa") + private static Object[][] nfaConfigs() { + final Object[][] result = new Object[Output.values().length][]; + + // use for-each loop + switch case to make compiler check for completeness + for (Output value : Output.values()) { + result[value.ordinal()] = switch (value) { + case AUT -> new Object[] {AUT_CMD, "/ser/nfa.aut", false}; + case BA -> new Object[] {BA_CMD, "/ser/nfa.ba", false}; + case DOT -> new Object[] {DOT_CMD, "/ser/nfa.dot", false}; + case LEARNLIBV2 -> new Object[] {LV2_CMD, "/ser/nfa.lv2", true}; + case MATA -> new Object[] {MATA_CMD, "/ser/nfa.mata", false}; + case SAF -> new Object[] {SAF_CMD, "/ser/nfa.saf", false}; + case TAF -> new Object[] {TAF_CMD, "/ser/nfa.taf", true}; + }; + } + + return result; + } + + @Test(dataProvider = "nfa") + public void testNFASerializers(String[] args, String resource, boolean shouldFail) throws IOException { + final Options options = Util.parseOptions(cmd, args); + + if (shouldFail) { + Assert.assertThrows(() -> SerializerFactory.getNFASerializer(options)); + } else { + final InputModelSerializer> nfaSerializer = + SerializerFactory.getNFASerializer(options); + final CompactNFA nfa = Util.getExampleNFA(); + + testSerializer(nfaSerializer, nfa, nfa.getInputAlphabet(), resource); + } + } + + @DataProvider(name = "sba") + private static Object[][] sbaConfigs() { + final Object[][] result = new Object[Output.values().length][]; + + // use for-each loop + switch case to make compiler check for completeness + for (Output value : Output.values()) { + result[value.ordinal()] = switch (value) { + case AUT -> new Object[] {AUT_CMD, "/ser/sba.aut", true}; + case BA -> new Object[] {BA_CMD, "/ser/sba.ba", true}; + case DOT -> new Object[] {DOT_CMD, "/ser/sba.dot", false}; + case LEARNLIBV2 -> new Object[] {LV2_CMD, "/ser/sba.lv2", true}; + case MATA -> new Object[] {MATA_CMD, "/ser/sba.mata", true}; + case SAF -> new Object[] {SAF_CMD, "/ser/sba.saf", true}; + case TAF -> new Object[] {TAF_CMD, "/ser/sba.taf", true}; + }; + } + + return result; + } + + @Test(dataProvider = "sba") + public void testSBASerializers(String[] args, String resource, boolean shouldFail) throws IOException { + final Options options = Util.parseOptions(cmd, args); + + if (shouldFail) { + Assert.assertThrows(() -> SerializerFactory.getSBASerializer(options)); + } else { + final InputModelSerializer> sbaSerializer = + SerializerFactory.getSBASerializer(options); + final SBA sba = Util.getExampleSBA(); + + testSerializer(sbaSerializer, sba, sba.getInputAlphabet(), resource); + } + } + + @DataProvider(name = "spa") + private static Object[][] spaConfigs() { + final Object[][] result = new Object[Output.values().length][]; + + // use for-each loop + switch case to make compiler check for completeness + for (Output value : Output.values()) { + result[value.ordinal()] = switch (value) { + case AUT -> new Object[] {AUT_CMD, "/ser/spa.aut", true}; + case BA -> new Object[] {BA_CMD, "/ser/spa.ba", true}; + case DOT -> new Object[] {DOT_CMD, "/ser/spa.dot", false}; + case LEARNLIBV2 -> new Object[] {LV2_CMD, "/ser/spa.lv2", true}; + case MATA -> new Object[] {MATA_CMD, "/ser/spa.mata", true}; + case SAF -> new Object[] {SAF_CMD, "/ser/spa.saf", true}; + case TAF -> new Object[] {TAF_CMD, "/ser/spa.taf", true}; + }; + } + + return result; + } + + @Test(dataProvider = "spa") + public void testSPASerializers(String[] args, String resource, boolean shouldFail) throws IOException { + final Options options = Util.parseOptions(cmd, args); + + if (shouldFail) { + Assert.assertThrows(() -> SerializerFactory.getSPASerializer(options)); + } else { + final InputModelSerializer> spaSerializer = + SerializerFactory.getSPASerializer(options); + final SPA spa = Util.getExampleSPA(); + + testSerializer(spaSerializer, spa, spa.getInputAlphabet(), resource); + } + } + + @DataProvider(name = "spmm") + private static Object[][] spmmConfigs() { + final Object[][] result = new Object[Output.values().length][]; + + // use for-each loop + switch case to make compiler check for completeness + for (Output value : Output.values()) { + result[value.ordinal()] = switch (value) { + case AUT -> new Object[] {AUT_CMD, "/ser/spmm.aut", true}; + case BA -> new Object[] {BA_CMD, "/ser/spmm.ba", true}; + case DOT -> new Object[] {DOT_CMD, "/ser/spmm.dot", false}; + case LEARNLIBV2 -> new Object[] {LV2_CMD, "/ser/spmm.lv2", true}; + case MATA -> new Object[] {MATA_CMD, "/ser/spmm.mata", true}; + case SAF -> new Object[] {SAF_CMD, "/ser/spmm.saf", true}; + case TAF -> new Object[] {TAF_CMD, "/ser/spmm.taf", true}; + }; + } + + return result; + } + + @Test(dataProvider = "spmm") + public void testSPMMSerializers(String[] args, String resource, boolean shouldFail) throws IOException { + final Options options = Util.parseOptions(cmd, args); + + if (shouldFail) { + Assert.assertThrows(() -> SerializerFactory.getSPMMSerializer(options)); + } else { + final InputModelSerializer> spmmSerializer = + SerializerFactory.getSPMMSerializer(options); + final SPMM spmm = Util.getExampleSPMM(); + + testSerializer(spmmSerializer, spmm, spmm.getInputAlphabet(), resource); + } + } + + @DataProvider(name = "vpa") + private static Object[][] vpaConfigs() { + final Object[][] result = new Object[Output.values().length][]; + + // use for-each loop + switch case to make compiler check for completeness + for (Output value : Output.values()) { + result[value.ordinal()] = switch (value) { + case AUT -> new Object[] {AUT_CMD, "/ser/vpa.aut", true}; + case BA -> new Object[] {BA_CMD, "/ser/vpa.ba", true}; + case DOT -> new Object[] {DOT_CMD, "/ser/vpa.dot", false}; + case LEARNLIBV2 -> new Object[] {LV2_CMD, "/ser/vpa.lv2", true}; + case MATA -> new Object[] {MATA_CMD, "/ser/vpa.mata", true}; + case SAF -> new Object[] {SAF_CMD, "/ser/vpa.saf", true}; + case TAF -> new Object[] {TAF_CMD, "/ser/vpa.taf", true}; + }; + } + + return result; + } + + @Test(dataProvider = "vpa") + public void testVPASerializers(String[] args, String resource, boolean shouldFail) throws IOException { + final Options options = Util.parseOptions(cmd, args); + + if (shouldFail) { + Assert.assertThrows(() -> SerializerFactory.getVPASerializer(options)); + } else { + final InputModelSerializer> vpaSerializer = + SerializerFactory.getVPASerializer(options); + final OneSEVPA vpa = Util.getExampleVPA(); + + testSerializer(vpaSerializer, vpa, vpa.getInputAlphabet(), resource); + } + } + + private void testSerializer(InputModelSerializer serializer, + M model, + Alphabet alphabet, + String resource) throws IOException { + try (ByteArrayOutputStream actual = new ByteArrayOutputStream(); + ByteArrayOutputStream expected = new ByteArrayOutputStream(); + InputStream is = SerializationFactoryTest.class.getResourceAsStream(resource)) { + + serializer.writeModel(actual, model, alphabet); + + Assert.assertNotNull(is); + is.transferTo(expected); + + Assert.assertEquals(actual.toString(), expected.toString(), resource); + } + } + +} diff --git a/cli/src/test/java/de/learnlib/cli/util/AcceptorNullOracle.java b/cli/src/test/java/de/learnlib/cli/util/AcceptorNullOracle.java new file mode 100644 index 000000000..5fd1098e1 --- /dev/null +++ b/cli/src/test/java/de/learnlib/cli/util/AcceptorNullOracle.java @@ -0,0 +1,27 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.util; + +import de.learnlib.oracle.SingleQueryOracle; +import net.automatalib.word.Word; + +public class AcceptorNullOracle implements SingleQueryOracle { + + @Override + public Boolean answerQuery(Word prefix, Word suffix) { + return false; + } +} diff --git a/cli/src/test/java/de/learnlib/cli/util/AdaptiveNullOracle.java b/cli/src/test/java/de/learnlib/cli/util/AdaptiveNullOracle.java new file mode 100644 index 000000000..3ccb400a8 --- /dev/null +++ b/cli/src/test/java/de/learnlib/cli/util/AdaptiveNullOracle.java @@ -0,0 +1,32 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.util; + +import de.learnlib.oracle.SingleAdaptiveMembershipOracle; +import de.learnlib.query.AdaptiveQuery; +import de.learnlib.query.AdaptiveQuery.Response; + +public class AdaptiveNullOracle implements SingleAdaptiveMembershipOracle { + + @Override + public void processQuery(AdaptiveQuery query) { + Response response; + do { + query.getInput(); // progress input + response = query.processOutput("err"); + } while (response != Response.FINISHED); + } +} diff --git a/cli/src/test/java/de/learnlib/cli/util/TransducerNullOracle.java b/cli/src/test/java/de/learnlib/cli/util/TransducerNullOracle.java new file mode 100644 index 000000000..b29c44283 --- /dev/null +++ b/cli/src/test/java/de/learnlib/cli/util/TransducerNullOracle.java @@ -0,0 +1,28 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.util; + +import de.learnlib.oracle.SingleQueryOracle; +import net.automatalib.word.Word; +import net.automatalib.word.WordBuilder; + +public class TransducerNullOracle implements SingleQueryOracle> { + + @Override + public Word answerQuery(Word prefix, Word suffix) { + return new WordBuilder<>("error", suffix.length()).toWord(); + } +} diff --git a/cli/src/test/java/de/learnlib/cli/util/Util.java b/cli/src/test/java/de/learnlib/cli/util/Util.java new file mode 100644 index 000000000..f0537546f --- /dev/null +++ b/cli/src/test/java/de/learnlib/cli/util/Util.java @@ -0,0 +1,147 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.cli.util; + +import java.util.Map; +import java.util.Random; + +import de.learnlib.cli.option.Options; +import net.automatalib.alphabet.ProceduralInputAlphabet; +import net.automatalib.alphabet.VPAlphabet; +import net.automatalib.alphabet.impl.Alphabets; +import net.automatalib.alphabet.impl.DefaultProceduralInputAlphabet; +import net.automatalib.alphabet.impl.DefaultVPAlphabet; +import net.automatalib.automaton.fsa.impl.CompactDFA; +import net.automatalib.automaton.fsa.impl.CompactNFA; +import net.automatalib.automaton.procedural.SBA; +import net.automatalib.automaton.procedural.SPA; +import net.automatalib.automaton.procedural.SPMM; +import net.automatalib.automaton.procedural.impl.StackSBA; +import net.automatalib.automaton.procedural.impl.StackSPA; +import net.automatalib.automaton.procedural.impl.StackSPMM; +import net.automatalib.automaton.transducer.impl.CompactMealy; +import net.automatalib.automaton.vpa.OneSEVPA; +import net.automatalib.util.automaton.builder.AutomatonBuilders; +import net.automatalib.util.automaton.fsa.MutableDFAs; +import net.automatalib.util.automaton.random.RandomAutomata; +import net.automatalib.util.automaton.transducer.MutableMealyMachines; +import picocli.CommandLine; + +public final class Util { + + private Util() { + // prevent instantiation + } + + public static Options parseOptions(CommandLine cmd, String... args) { + return (Options) cmd.parseArgs(args).commandSpec().mixins().get("options").userObject(); + } + + public static CompactDFA getExampleDFA() { + // @formatter:off + return AutomatonBuilders.newDFA(Alphabets.closedCharStringRange('a', 'b')) + .from("s0").on("a", "b").to("s1") + .from("s1").on("a", "b").to("s0") + .withInitial("s0") + .withAccepting("s1") + .create(); + // @formatter:on + } + + public static CompactMealy getExampleMealy() { + // @formatter:off + return AutomatonBuilders.newMealy(Alphabets.closedCharStringRange('a', 'b')) + .from("s0").on("a").withOutput("97").loop() + .from("s0").on("b").withOutput("98").loop() + .withInitial("s0") + .create(); + // @formatter:on + } + + public static CompactNFA getExampleNFA() { + // @formatter:off + return AutomatonBuilders.newNFA(Alphabets.closedCharStringRange('a', 'b')) + .from("s0").on("a", "b").to("s1") + .from("s1").on("a", "b").to("s0") + .withInitial("s0") + .withAccepting("s1") + .create(); + // @formatter:on + } + + public static SBA getExampleSBA() { + final ProceduralInputAlphabet alphabet = + new DefaultProceduralInputAlphabet<>(Alphabets.closedCharStringRange('a', 'b'), + Alphabets.singleton("S"), + "R"); + // @formatter:off + final var dfa = AutomatonBuilders.newDFA(alphabet) + .from("s0").on("R").to("s1") + .on("a", "b").to("s2") + .from("s2").on("R").to("s3") + .withInitial("s0") + .withAccepting("s0", "s2", "s3") + .create(); + // @formatter:on + MutableDFAs.complete(dfa, alphabet, true); + + return new StackSBA<>(alphabet, "S", Map.of("S", dfa)); + } + + public static SPA getExampleSPA() { + final ProceduralInputAlphabet alphabet = + new DefaultProceduralInputAlphabet<>(Alphabets.closedCharStringRange('a', 'b'), + Alphabets.singleton("S"), + "R"); + // @formatter:off + final var dfa = AutomatonBuilders.newDFA(alphabet.getProceduralAlphabet()) + .from("s0").on("a", "b").to("s1") + .on("S").to("s0") + .from("s1").on("a", "b").to("s0") + .on("S").to("s0") + .withInitial("s0") + .withAccepting("s1") + .create(); + // @formatter:on + return new StackSPA<>(alphabet, "S", Map.of("S", dfa)); + } + + public static SPMM getExampleSPMM() { + final ProceduralInputAlphabet alphabet = + new DefaultProceduralInputAlphabet<>(Alphabets.closedCharStringRange('a', 'b'), + Alphabets.singleton("S"), + "R"); + final String error = "error"; + + // @formatter:off + final var mealy = AutomatonBuilders.newMealy(alphabet) + .from("s0").on("a").withOutput("97").to("s1") + .on("b").withOutput("98").to("s1") + .from("s1").on("R").withOutput("ok").to("s2") + .withInitial("s0") + .create(); + // @formatter:on + MutableMealyMachines.complete(mealy, alphabet, error, true); + return new StackSPMM<>(alphabet, "S", "✓", error, Map.of("S", mealy)); + } + + public static OneSEVPA getExampleVPA() { + final VPAlphabet alphabet = new DefaultVPAlphabet<>(Alphabets.singleton("a"), + Alphabets.singleton("S"), + Alphabets.fromArray("R1", "R2")); + return RandomAutomata.randomOneSEVPA(new Random(42), 2, alphabet, 0.5, 0.5, true); + } +} diff --git a/cli/src/test/resources/logback-test.xml b/cli/src/test/resources/logback-test.xml new file mode 100644 index 000000000..90b5f4a99 --- /dev/null +++ b/cli/src/test/resources/logback-test.xml @@ -0,0 +1,28 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %25.25(%logger{25}) - %msg %n + + + + + + + diff --git a/cli/src/test/resources/ser/dfa.aut b/cli/src/test/resources/ser/dfa.aut new file mode 100644 index 000000000..1a06e6416 --- /dev/null +++ b/cli/src/test/resources/ser/dfa.aut @@ -0,0 +1,5 @@ +des (0, 4, 2) +(0, "a", 1) +(0, "b", 1) +(1, "a", 0) +(1, "b", 0) diff --git a/cli/src/test/resources/ser/dfa.ba b/cli/src/test/resources/ser/dfa.ba new file mode 100644 index 000000000..fdad59e44 --- /dev/null +++ b/cli/src/test/resources/ser/dfa.ba @@ -0,0 +1,6 @@ +0 +a,0->1 +b,0->1 +a,1->0 +b,1->0 +1 diff --git a/cli/src/test/resources/ser/dfa.dot b/cli/src/test/resources/ser/dfa.dot new file mode 100644 index 000000000..66dbba275 --- /dev/null +++ b/cli/src/test/resources/ser/dfa.dot @@ -0,0 +1,13 @@ +digraph g { + + s0 [shape="circle" label="0"]; + s1 [shape="doublecircle" label="1"]; + s0 -> s1 [label="a"]; + s0 -> s1 [label="b"]; + s1 -> s0 [label="a"]; + s1 -> s0 [label="b"]; + +__start0 [label="" shape="none" width="0" height="0"]; +__start0 -> s0; + +} diff --git a/cli/src/test/resources/ser/dfa.lv2 b/cli/src/test/resources/ser/dfa.lv2 new file mode 100644 index 000000000..268f54f4a --- /dev/null +++ b/cli/src/test/resources/ser/dfa.lv2 @@ -0,0 +1,4 @@ +2 2 +0 1 +1 1 +0 0 diff --git a/cli/src/test/resources/ser/dfa.mata b/cli/src/test/resources/ser/dfa.mata new file mode 100644 index 000000000..616eb360c --- /dev/null +++ b/cli/src/test/resources/ser/dfa.mata @@ -0,0 +1,9 @@ +@NFA-explicit +%Alphabet 0 1 +%States q0 q1 +%Initial q0 +%Final q1 +q0 0 q1 +q0 1 q1 +q1 0 q0 +q1 1 q0 diff --git a/cli/src/test/resources/ser/dfa.saf b/cli/src/test/resources/ser/dfa.saf new file mode 100644 index 000000000..3fb36f2da Binary files /dev/null and b/cli/src/test/resources/ser/dfa.saf differ diff --git a/cli/src/test/resources/ser/dfa.taf b/cli/src/test/resources/ser/dfa.taf new file mode 100644 index 000000000..ceeacb3c2 --- /dev/null +++ b/cli/src/test/resources/ser/dfa.taf @@ -0,0 +1,8 @@ +dfa {a,b} { + s0 [initial] { + {a,b} -> s1 + } + s1 [accepting] { + {a,b} -> s0 + } +} diff --git a/cli/src/test/resources/ser/mealy.dot b/cli/src/test/resources/ser/mealy.dot new file mode 100644 index 000000000..7e910c54f --- /dev/null +++ b/cli/src/test/resources/ser/mealy.dot @@ -0,0 +1,10 @@ +digraph g { + + s0 [shape="circle" label="0"]; + s0 -> s0 [label="a / 97"]; + s0 -> s0 [label="b / 98"]; + +__start0 [label="" shape="none" width="0" height="0"]; +__start0 -> s0; + +} diff --git a/cli/src/test/resources/ser/mealy.saf b/cli/src/test/resources/ser/mealy.saf new file mode 100644 index 000000000..4469cf8d7 Binary files /dev/null and b/cli/src/test/resources/ser/mealy.saf differ diff --git a/cli/src/test/resources/ser/mealy.taf b/cli/src/test/resources/ser/mealy.taf new file mode 100644 index 000000000..1426d4a10 --- /dev/null +++ b/cli/src/test/resources/ser/mealy.taf @@ -0,0 +1,6 @@ +mealy {a,b} { + s0 [initial] { + b / 98 -> s0 + a / 97 -> s0 + } +} diff --git a/cli/src/test/resources/ser/mealy_large.dot b/cli/src/test/resources/ser/mealy_large.dot new file mode 100644 index 000000000..e934259fe --- /dev/null +++ b/cli/src/test/resources/ser/mealy_large.dot @@ -0,0 +1,19 @@ +digraph g { + + s0 [shape="circle" label="s0"]; + s1 [shape="circle" label="s1"]; + s2 [shape="circle" label="s2"]; + s3 [shape="circle" label="s3"]; + s0 -> s1 [label="a / 97"]; + s0 -> s1 [label="b / 98"]; + s1 -> s2 [label="a / 97"]; + s1 -> s2 [label="b / 98"]; + s2 -> s3 [label="a / 97"]; + s2 -> s3 [label="b / 98"]; + s3 -> s3 [label="a / error"]; + s3 -> s3 [label="b / error"]; + +__start0 [label="" shape="none" width="0" height="0"]; +__start0 -> s0; + +} diff --git a/cli/src/test/resources/ser/nfa.aut b/cli/src/test/resources/ser/nfa.aut new file mode 100644 index 000000000..1a06e6416 --- /dev/null +++ b/cli/src/test/resources/ser/nfa.aut @@ -0,0 +1,5 @@ +des (0, 4, 2) +(0, "a", 1) +(0, "b", 1) +(1, "a", 0) +(1, "b", 0) diff --git a/cli/src/test/resources/ser/nfa.ba b/cli/src/test/resources/ser/nfa.ba new file mode 100644 index 000000000..fdad59e44 --- /dev/null +++ b/cli/src/test/resources/ser/nfa.ba @@ -0,0 +1,6 @@ +0 +a,0->1 +b,0->1 +a,1->0 +b,1->0 +1 diff --git a/cli/src/test/resources/ser/nfa.dot b/cli/src/test/resources/ser/nfa.dot new file mode 100644 index 000000000..66dbba275 --- /dev/null +++ b/cli/src/test/resources/ser/nfa.dot @@ -0,0 +1,13 @@ +digraph g { + + s0 [shape="circle" label="0"]; + s1 [shape="doublecircle" label="1"]; + s0 -> s1 [label="a"]; + s0 -> s1 [label="b"]; + s1 -> s0 [label="a"]; + s1 -> s0 [label="b"]; + +__start0 [label="" shape="none" width="0" height="0"]; +__start0 -> s0; + +} diff --git a/cli/src/test/resources/ser/nfa.mata b/cli/src/test/resources/ser/nfa.mata new file mode 100644 index 000000000..616eb360c --- /dev/null +++ b/cli/src/test/resources/ser/nfa.mata @@ -0,0 +1,9 @@ +@NFA-explicit +%Alphabet 0 1 +%States q0 q1 +%Initial q0 +%Final q1 +q0 0 q1 +q0 1 q1 +q1 0 q0 +q1 1 q0 diff --git a/cli/src/test/resources/ser/nfa.saf b/cli/src/test/resources/ser/nfa.saf new file mode 100644 index 000000000..6e4ad0764 Binary files /dev/null and b/cli/src/test/resources/ser/nfa.saf differ diff --git a/cli/src/test/resources/ser/sba.dot b/cli/src/test/resources/ser/sba.dot new file mode 100644 index 000000000..0ae8e991c --- /dev/null +++ b/cli/src/test/resources/ser/sba.dot @@ -0,0 +1,27 @@ +digraph g { + + s0 [shape="doublecircle" label="S 0"]; + s1 [shape="circle" label="S 1"]; + s2 [shape="doublecircle" label="S 2"]; + s3 [shape="doublecircle" label="S 3"]; + s0 -> s1 [label="a"]; + s0 -> s1 [label="b"]; + s0 -> s1 [style="bold" label="S"]; + s0 -> s1 [style="bold" label="R"]; + s1 -> s1 [label="a"]; + s1 -> s1 [label="b"]; + s1 -> s1 [style="bold" label="S"]; + s1 -> s1 [style="bold" label="R"]; + s2 -> s3 [label="a"]; + s2 -> s3 [label="b"]; + s2 -> s1 [style="bold" label="S"]; + s2 -> s1 [style="bold" label="R"]; + s3 -> s1 [label="a"]; + s3 -> s1 [label="b"]; + s3 -> s1 [style="bold" label="S"]; + s3 -> s0 [style="bold" label="R"]; + +__start0 [label="" shape="none" width="0" height="0"]; +__start0 -> s2; + +} diff --git a/cli/src/test/resources/ser/spa.dot b/cli/src/test/resources/ser/spa.dot new file mode 100644 index 000000000..2ae6af383 --- /dev/null +++ b/cli/src/test/resources/ser/spa.dot @@ -0,0 +1,15 @@ +digraph g { + + s0 [shape="circle" label="S 0"]; + s1 [shape="doublecircle" label="S 1"]; + s0 -> s1 [label="a"]; + s0 -> s1 [label="b"]; + s0 -> s0 [style="bold" label="S"]; + s1 -> s0 [label="a"]; + s1 -> s0 [label="b"]; + s1 -> s0 [style="bold" label="S"]; + +__start0 [label="" shape="none" width="0" height="0"]; +__start0 -> s0; + +} diff --git a/cli/src/test/resources/ser/spmm.dot b/cli/src/test/resources/ser/spmm.dot new file mode 100644 index 000000000..8c76dc721 --- /dev/null +++ b/cli/src/test/resources/ser/spmm.dot @@ -0,0 +1,22 @@ +digraph g { + + s0 [shape="circle" label="S 0"]; + s1 [shape="circle" label="S 1"]; + s2 [shape="circle" label="S 2"]; + s0 -> s1 [label="a / 97"]; + s0 -> s1 [label="b / 98"]; + s0 -> s2 [style="bold" label="S / error"]; + s0 -> s2 [style="bold" label="R / error"]; + s1 -> s2 [label="a / error"]; + s1 -> s2 [label="b / error"]; + s1 -> s2 [style="bold" label="S / error"]; + s1 -> s2 [style="bold" label="R / ok"]; + s2 -> s2 [label="a / error"]; + s2 -> s2 [label="b / error"]; + s2 -> s2 [style="bold" label="S / error"]; + s2 -> s2 [style="bold" label="R / error"]; + +__start0 [label="" shape="none" width="0" height="0"]; +__start0 -> s0; + +} diff --git a/cli/src/test/resources/ser/vpa.dot b/cli/src/test/resources/ser/vpa.dot new file mode 100644 index 000000000..000ecbb00 --- /dev/null +++ b/cli/src/test/resources/ser/vpa.dot @@ -0,0 +1,21 @@ +digraph g { + + s0 [shape="circle" label="L0"]; + s1 [shape="doublecircle" label="L1"]; + s0 -> s0 [label="S"]; + s0 -> s1 [label="a"]; + s0 -> s1 [label="R1/(L0,S)"]; + s0 -> s1 [label="R1/(L1,S)"]; + s0 -> s0 [label="R2/(L0,S)"]; + s0 -> s0 [label="R2/(L1,S)"]; + s1 -> s0 [label="S"]; + s1 -> s0 [label="a"]; + s1 -> s0 [label="R1/(L0,S)"]; + s1 -> s1 [label="R1/(L1,S)"]; + s1 -> s0 [label="R2/(L0,S)"]; + s1 -> s0 [label="R2/(L1,S)"]; + +__start0 [label="" shape="none" width="0" height="0"]; +__start0 -> s0; + +} diff --git a/cli/src/test/resources/sul/sba.py b/cli/src/test/resources/sul/sba.py new file mode 100755 index 000000000..c6e0e8a57 --- /dev/null +++ b/cli/src/test/resources/sul/sba.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +import sys + +def main(): + argv = len(sys.argv) + + if argv > 1: + if sys.argv[1] == "S": + print("ok", end="") + if argv > 2: + arg = sys.argv[2] + if arg == "a" or arg == "b": + print(" ", end="") + print(sum([ord(c) for c in arg]), end="") + if argv > 3: + if sys.argv[3] == "R": + print(" ok", end="") + if argv > 4: + print(" error" * (argv - 4), end="") + print() + sys.exit(1) + else: + print() + sys.exit(0) + else: + print(" error" * (argv - 3), end="") + else: + sys.exit(0) + else: + print(" error" * (argv - 2), end="") + else: + sys.exit(0) + else: + print(" error" * argv, end="") + + print() + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/cli/src/test/resources/sul/spa.py b/cli/src/test/resources/sul/spa.py new file mode 100755 index 000000000..9c13c9433 --- /dev/null +++ b/cli/src/test/resources/sul/spa.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +import sys + +def main(): + argv = len(sys.argv) + + if argv > 1: + if sys.argv[1] == "S": + print("ok", end="") + if argv > 2: + arg = sys.argv[2] + if arg == "a" or arg == "b": + print(" ", end="") + print(sum([ord(c) for c in arg]), end="") + if argv > 3: + if sys.argv[3] == "R": + print(" ok", end="") + if argv > 4: + print(" error" * (argv - 4), end="") + print() + sys.exit(1) + else: + print() + sys.exit(0) + else: + print(" error" * (argv - 3), end="") + else: + print(" error" * (argv - 2), end="") + else: + print(" error" * argv, end="") + + print() + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/cli/src/test/resources/sul/stateful.py b/cli/src/test/resources/sul/stateful.py new file mode 100755 index 000000000..db13e295f --- /dev/null +++ b/cli/src/test/resources/sul/stateful.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +from pathlib import Path + +import sys +import pickle +import os.path + +def main(): + folder = Path(sys.argv[0]).parent + path = os.path.join(folder, "state.p") + argv = 1 + + if os.path.isfile(path): + argv = pickle.load(open(path, "rb")) + + if len(sys.argv) > 1: + for arg in sys.argv[1:]: + if arg == "reset": + argv = 1 + else: + argv += 1 + print(sum([ord(c) for c in arg])) + + pickle.dump(argv, open(path, "wb")) + sys.exit(argv % 2) + +if __name__ == "__main__": + main() diff --git a/cli/src/test/resources/sul/stateless.py b/cli/src/test/resources/sul/stateless.py new file mode 100755 index 000000000..9e25a3824 --- /dev/null +++ b/cli/src/test/resources/sul/stateless.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +import sys + +def main(): + argv = len(sys.argv) + + if argv > 1: + for arg in sys.argv[1:]: + print(sum([ord(c) for c in arg])) + + sys.exit(argv % 2) + +if __name__ == "__main__": + main() diff --git a/cli/src/test/resources/sul/stateless_broken.py b/cli/src/test/resources/sul/stateless_broken.py new file mode 100755 index 000000000..7b0f6e583 --- /dev/null +++ b/cli/src/test/resources/sul/stateless_broken.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +import sys + +def main(): + argv = len(sys.argv) + + if argv > 2: + for arg in sys.argv[2:]: + print(sum([ord(c) for c in arg])) + + sys.exit(argv % 2) + +if __name__ == "__main__": + main() diff --git a/cli/src/test/resources/sul/stateless_large.py b/cli/src/test/resources/sul/stateless_large.py new file mode 100755 index 000000000..1672d688f --- /dev/null +++ b/cli/src/test/resources/sul/stateless_large.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +import sys + +def main(): + argv = len(sys.argv) + limit = 3 + + if argv > 1: + for idx, arg in enumerate(sys.argv[1:]): + if idx < limit: + print(sum([ord(c) for c in arg])) + else: + print("error") + + if (argv < limit): + sys.exit(argv % 2) + else: + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/commons/util/pom.xml b/commons/util/pom.xml index 894f14032..03f7c3a3d 100644 --- a/commons/util/pom.xml +++ b/commons/util/pom.xml @@ -61,6 +61,12 @@ limitations under the License. slf4j-api + + + de.learnlib.tooling + annotations + + org.mockito diff --git a/commons/util/src/main/java/de/learnlib/util/Experiment.java b/commons/util/src/main/java/de/learnlib/util/Experiment.java index 255c4897c..b731acd48 100644 --- a/commons/util/src/main/java/de/learnlib/util/Experiment.java +++ b/commons/util/src/main/java/de/learnlib/util/Experiment.java @@ -15,19 +15,32 @@ */ package de.learnlib.util; -import java.util.Collection; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import de.learnlib.algorithm.LearningAlgorithm; +import de.learnlib.algorithm.LearningAlgorithm.DFALearner; +import de.learnlib.algorithm.LearningAlgorithm.MealyLearner; +import de.learnlib.algorithm.LearningAlgorithm.MooreLearner; import de.learnlib.logging.Category; import de.learnlib.oracle.EquivalenceOracle; +import de.learnlib.oracle.EquivalenceOracle.DFAEquivalenceOracle; +import de.learnlib.oracle.EquivalenceOracle.MealyEquivalenceOracle; +import de.learnlib.oracle.EquivalenceOracle.MooreEquivalenceOracle; import de.learnlib.query.DefaultQuery; import de.learnlib.statistic.Statistics; import de.learnlib.statistic.StatisticsKey; import de.learnlib.statistic.StatisticsService; +import de.learnlib.tooling.annotation.refinement.GenerateRefinement; +import de.learnlib.tooling.annotation.refinement.Generic; +import de.learnlib.tooling.annotation.refinement.Mapping; +import net.automatalib.alphabet.Alphabet; import net.automatalib.automaton.concept.FiniteRepresentation; import net.automatalib.automaton.fsa.DFA; import net.automatalib.automaton.transducer.MealyMachine; import net.automatalib.automaton.transducer.MooreMachine; +import net.automatalib.serialization.InputModelSerializer; import net.automatalib.word.Word; import org.checkerframework.checker.nullness.qual.Nullable; import org.slf4j.Logger; @@ -38,8 +51,47 @@ * * @param * the automaton type + * @param + * input symbol type + * @param + * output domain type */ -public class Experiment { +@GenerateRefinement(name = "DFAExperiment", + generics = @Generic(value = "I", desc = "input symbol type"), + parentGenerics = {@Generic(clazz = DFA.class, generics = {"?", "I"}), + @Generic("I"), + @Generic(clazz = Boolean.class)}, + typeMappings = {@Mapping(from = LearningAlgorithm.class, + to = DFALearner.class, + generics = @Generic("I")), + @Mapping(from = EquivalenceOracle.class, + to = DFAEquivalenceOracle.class, + generics = @Generic("I"))}) +@GenerateRefinement(name = "MealyExperiment", + generics = {@Generic(value = "I", desc = "input symbol type"), + @Generic(value = "O", desc = "output symbol type")}, + parentGenerics = {@Generic(clazz = MealyMachine.class, generics = {"?", "I", "?", "O"}), + @Generic("I"), + @Generic(clazz = Word.class, generics = "O")}, + typeMappings = {@Mapping(from = LearningAlgorithm.class, + to = MealyLearner.class, + generics = {@Generic("I"), @Generic("O")}), + @Mapping(from = EquivalenceOracle.class, + to = MealyEquivalenceOracle.class, + generics = {@Generic("I"), @Generic("O")})}) +@GenerateRefinement(name = "MooreExperiment", + generics = {@Generic(value = "I", desc = "input symbol type"), + @Generic(value = "O", desc = "output symbol type")}, + parentGenerics = {@Generic(clazz = MooreMachine.class, generics = {"?", "I", "?", "O"}), + @Generic("I"), + @Generic(clazz = Word.class, generics = "O")}, + typeMappings = {@Mapping(from = LearningAlgorithm.class, + to = MooreLearner.class, + generics = {@Generic("I"), @Generic("O")}), + @Mapping(from = EquivalenceOracle.class, + to = MooreEquivalenceOracle.class, + generics = {@Generic("I"), @Generic("O")})}) +public class Experiment { /** * The {@link StatisticsKey} this class uses for clocking the duration of the exploration phase of the learning @@ -64,31 +116,58 @@ public class Experiment { */ public static final StatisticsKey KEY_FINAL_SIZE = new StatisticsKey("exp-hyp-size", "Size of final hypothesis"); - private static final Logger LOGGER = LoggerFactory.getLogger(Experiment.class); - private final ExperimentImpl impl; - private @Nullable A finalHypothesis; + protected static final Logger LOGGER = LoggerFactory.getLogger(Experiment.class); - public Experiment(LearningAlgorithm learningAlgorithm, - EquivalenceOracle equivalenceAlgorithm, - Collection inputs) { - this.impl = new ExperimentImpl<>(learningAlgorithm, equivalenceAlgorithm, inputs); - } + protected final LearningAlgorithm learningAlgorithm; + protected final EquivalenceOracle equivalenceAlgorithm; + protected final Alphabet inputs; + protected final @Nullable InputModelSerializer serializer; + protected final StatisticsService statistics; + private int rounds; + private @Nullable A finalHypothesis; /** - * Run the experiment, once. + * Constructor. Delegates to + * {@link Experiment#Experiment(LearningAlgorithm, EquivalenceOracle, Alphabet, InputModelSerializer)} using + * {@code null} for {@code serializer}. * - * @return the final hypothesis + * @param learningAlgorithm + * the learning algorithm to use in this experiment + * @param equivalenceAlgorithm + * the strategy for finding counterexamples + * @param inputs + * the inputs to consider for exploration * - * @throws IllegalStateException - * if invoked more than once + * @see Experiment#Experiment(LearningAlgorithm, EquivalenceOracle, Alphabet, InputModelSerializer) */ - public A run() { - if (this.finalHypothesis != null) { - throw new IllegalStateException("Experiment has already been run"); - } + public Experiment(LearningAlgorithm learningAlgorithm, + EquivalenceOracle equivalenceAlgorithm, + Alphabet inputs) { + this(learningAlgorithm, equivalenceAlgorithm, inputs, null); + } - finalHypothesis = impl.run(); - return finalHypothesis; + /** + * Constructor. Creates a new experiment to run. + * + * @param learningAlgorithm + * the learning algorithm to use in this experiment + * @param equivalenceAlgorithm + * the strategy for finding counterexamples + * @param inputs + * the inputs to consider for exploration + * @param serializer + * the serializer for logging intermediate hypotheses (may be {@code null} in case no such logging is + * wanted) + */ + public Experiment(LearningAlgorithm learningAlgorithm, + EquivalenceOracle equivalenceAlgorithm, + Alphabet inputs, + @Nullable InputModelSerializer serializer) { + this.learningAlgorithm = learningAlgorithm; + this.equivalenceAlgorithm = equivalenceAlgorithm; + this.inputs = inputs; + this.serializer = serializer; + this.statistics = Statistics.getService(); } /** @@ -99,7 +178,7 @@ public A run() { * @throws IllegalStateException * if the experiment has not been run yet */ - public A getFinalHypothesis() { + public final A getFinalHypothesis() { if (finalHypothesis == null) { throw new IllegalStateException("Experiment has not yet been run"); } @@ -107,92 +186,95 @@ public A getFinalHypothesis() { return finalHypothesis; } - private final class ExperimentImpl { - - private final LearningAlgorithm learningAlgorithm; - private final EquivalenceOracle equivalenceAlgorithm; - private final Collection inputs; - private final StatisticsService statistics; - private int rounds; - - ExperimentImpl(LearningAlgorithm learningAlgorithm, - EquivalenceOracle equivalenceAlgorithm, - Collection inputs) { - this.learningAlgorithm = learningAlgorithm; - this.equivalenceAlgorithm = equivalenceAlgorithm; - this.inputs = inputs; - this.statistics = Statistics.getService(); + /** + * Run the experiment, once. + * + * @return the final hypothesis + * + * @throws IllegalStateException + * if invoked more than once + */ + public final A run() { + if (this.finalHypothesis != null) { + throw new IllegalStateException("Experiment has already been run"); } - A run() { - rounds++; - statistics.increaseCounter(KEY_ROUNDS, Experiment.this); - LOGGER.info(Category.PHASE, "Starting round {}", rounds); - LOGGER.info(Category.PHASE, "Learning"); - - statistics.startOrResumeClock(KEY_DUR_LEARN, Experiment.this); - learningAlgorithm.startLearning(); - statistics.pauseClock(KEY_DUR_LEARN, Experiment.this); + finalHypothesis = runInternal(); + return finalHypothesis; + } - while (true) { - final A hyp = learningAlgorithm.getHypothesisModel(); + private A runInternal() { + rounds++; + statistics.increaseCounter(KEY_ROUNDS, this); + LOGGER.info(Category.PHASE, "Starting round {}", rounds); + LOGGER.info(Category.PHASE, "Learning"); - LOGGER.info(Category.PHASE, "Searching for counterexample"); + initializeLearning(); - statistics.startOrResumeClock(KEY_DUR_CEX, Experiment.this); - DefaultQuery ce = equivalenceAlgorithm.findCounterExample(hyp, inputs); - statistics.pauseClock(KEY_DUR_CEX, Experiment.this); + while (true) { + final A hyp = learningAlgorithm.getHypothesisModel(); - if (ce == null) { - statistics.setCounter(KEY_FINAL_SIZE, hyp.size(), Experiment.this); - return hyp; + if (serializer != null) { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + serializer.writeModel(baos, hyp, inputs); + LOGGER.info(Category.MODEL, "Intermediate hypothesis:\n{}", baos.toString(StandardCharsets.UTF_8)); + } catch (IOException e) { + LOGGER.warn("Couldn't write intermediate hypothesis", e); } + } - LOGGER.info(Category.COUNTEREXAMPLE, ce.getInput().toString()); - - // next round ... - rounds++; - statistics.increaseCounter(KEY_ROUNDS, Experiment.this); - LOGGER.info(Category.PHASE, "Starting round {}", rounds); - LOGGER.info(Category.PHASE, "Learning"); + LOGGER.info(Category.PHASE, "Searching for counterexample"); - statistics.startOrResumeClock(KEY_DUR_LEARN, Experiment.this); - final boolean refined = learningAlgorithm.refineHypothesis(ce); - statistics.pauseClock(KEY_DUR_LEARN, Experiment.this); + statistics.startOrResumeClock(KEY_DUR_CEX, this); + DefaultQuery ce = equivalenceAlgorithm.findCounterExample(hyp, inputs); + statistics.pauseClock(KEY_DUR_CEX, this); - assert refined; + if (ce == null) { + statistics.setCounter(KEY_FINAL_SIZE, hyp.size(), this); + return hyp; } - } - } - public static class DFAExperiment extends Experiment> { + LOGGER.info(Category.COUNTEREXAMPLE, ce.getInput().toString()); - public DFAExperiment(LearningAlgorithm, I, Boolean> learningAlgorithm, - EquivalenceOracle, I, Boolean> equivalenceAlgorithm, - Collection inputs) { - super(learningAlgorithm, equivalenceAlgorithm, inputs); - } + // next round ... + rounds++; + statistics.increaseCounter(KEY_ROUNDS, this); + LOGGER.info(Category.PHASE, "Starting round {}", rounds); + LOGGER.info(Category.PHASE, "Learning"); - } + statistics.startOrResumeClock(KEY_DUR_LEARN, this); + final boolean refined = learningAlgorithm.refineHypothesis(ce); + statistics.pauseClock(KEY_DUR_LEARN, this); - public static class MealyExperiment extends Experiment> { + assert refined; - public MealyExperiment(LearningAlgorithm, I, Word> learningAlgorithm, - EquivalenceOracle, I, Word> equivalenceAlgorithm, - Collection inputs) { - super(learningAlgorithm, equivalenceAlgorithm, inputs); + postRefinementHook(); } - } - public static class MooreExperiment extends Experiment> { - - public MooreExperiment(LearningAlgorithm, I, Word> learningAlgorithm, - EquivalenceOracle, I, Word> equivalenceAlgorithm, - Collection inputs) { - super(learningAlgorithm, equivalenceAlgorithm, inputs); - } + /** + * Utility method to access to current learning round. + * + * @return the current learning round + */ + protected final int getRound() { + return rounds; + } + /** + * Initializes the learning process. By default, this method calls {@link LearningAlgorithm#startLearning()}. + */ + protected void initializeLearning() { + statistics.startOrResumeClock(KEY_DUR_LEARN, this); + learningAlgorithm.startLearning(); + statistics.pauseClock(KEY_DUR_LEARN, this); } + /** + * Called upon calling {@link LearningAlgorithm#refineHypothesis(DefaultQuery)}. + */ + protected void postRefinementHook() { + // do nothing by default + } } diff --git a/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner/Adaptive2MembershipWrapper.java b/commons/util/src/main/java/de/learnlib/util/mealy/Adaptive2MembershipWrapper.java similarity index 87% rename from algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner/Adaptive2MembershipWrapper.java rename to commons/util/src/main/java/de/learnlib/util/mealy/Adaptive2MembershipWrapper.java index f8cef9822..db0497ab0 100644 --- a/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner/Adaptive2MembershipWrapper.java +++ b/commons/util/src/main/java/de/learnlib/util/mealy/Adaptive2MembershipWrapper.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.learnlib.algorithm.adt.learner; +package de.learnlib.util.mealy; import java.util.ArrayList; import java.util.Collection; @@ -22,7 +22,6 @@ import de.learnlib.oracle.MembershipOracle.MealyMembershipOracle; import de.learnlib.query.AdaptiveQuery; import de.learnlib.query.Query; -import de.learnlib.util.mealy.PresetAdaptiveQuery; import net.automatalib.word.Word; /** @@ -33,11 +32,11 @@ * @param * output symbol type */ -class Adaptive2MembershipWrapper implements MealyMembershipOracle { +public class Adaptive2MembershipWrapper implements MealyMembershipOracle { private final AdaptiveMembershipOracle oracle; - Adaptive2MembershipWrapper(AdaptiveMembershipOracle oracle) { + public Adaptive2MembershipWrapper(AdaptiveMembershipOracle oracle) { this.oracle = oracle; } diff --git a/commons/util/src/main/java/module-info.java b/commons/util/src/main/java/module-info.java index 7f59a5c2d..3a6a479f8 100644 --- a/commons/util/src/main/java/module-info.java +++ b/commons/util/src/main/java/module-info.java @@ -35,6 +35,7 @@ requires org.slf4j; // annotations are 'provided'-scoped and do not need to be loaded at runtime + requires static de.learnlib.tooling.annotation; requires static org.checkerframework.checker.qual; exports de.learnlib.util; diff --git a/commons/util/src/test/java/de/learnlib/util/ExperimentTest.java b/commons/util/src/test/java/de/learnlib/util/ExperimentTest.java index 603c5dfde..a87829eba 100644 --- a/commons/util/src/test/java/de/learnlib/util/ExperimentTest.java +++ b/commons/util/src/test/java/de/learnlib/util/ExperimentTest.java @@ -23,7 +23,6 @@ import de.learnlib.query.DefaultQuery; import de.learnlib.statistic.Statistics; import de.learnlib.statistic.StatisticsService; -import de.learnlib.util.Experiment.DFAExperiment; import net.automatalib.alphabet.Alphabet; import net.automatalib.alphabet.impl.Alphabets; import net.automatalib.automaton.fsa.DFA; diff --git a/distribution/pom.xml b/distribution/pom.xml index edf01ce31..723340b3c 100644 --- a/distribution/pom.xml +++ b/distribution/pom.xml @@ -227,7 +227,7 @@ limitations under the License. - jlink + integration-tests @@ -270,13 +270,19 @@ limitations under the License. - + de.learnlib learnlib-examples ${project.version} test + + de.learnlib + learnlib-cli + ${project.version} + test + diff --git a/distribution/src/it/jlink/pom.xml b/distribution/src/it/jlink/pom.xml index 09c1822c9..b96f1bcd3 100644 --- a/distribution/src/it/jlink/pom.xml +++ b/distribution/src/it/jlink/pom.xml @@ -39,10 +39,6 @@ limitations under the License. net.automatalib automata-brics - - net.automatalib - automata-modelchecking-m3c - net.automatalib automata-jung-visualizer @@ -56,7 +52,6 @@ limitations under the License. org.apache.maven.plugins maven-jlink-plugin - 3.1.0 distribution true diff --git a/examples/pom.xml b/examples/pom.xml index d8b48718d..5171641a6 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -192,8 +192,8 @@ limitations under the License. - org.apache.fury - fury-core + org.apache.fory + fory-core org.checkerframework diff --git a/examples/src/main/java/de/learnlib/example/Example1.java b/examples/src/main/java/de/learnlib/example/Example1.java index 13100f52c..9eba994a0 100644 --- a/examples/src/main/java/de/learnlib/example/Example1.java +++ b/examples/src/main/java/de/learnlib/example/Example1.java @@ -27,7 +27,7 @@ import de.learnlib.oracle.equivalence.DFAWMethodEQOracle; import de.learnlib.oracle.membership.DFASimulatorOracle; import de.learnlib.statistic.Statistics; -import de.learnlib.util.Experiment.DFAExperiment; +import de.learnlib.util.DFAExperiment; import net.automatalib.alphabet.Alphabet; import net.automatalib.alphabet.impl.Alphabets; import net.automatalib.automaton.fsa.DFA; diff --git a/examples/src/main/java/de/learnlib/example/Example2.java b/examples/src/main/java/de/learnlib/example/Example2.java index eda692c87..41b081259 100644 --- a/examples/src/main/java/de/learnlib/example/Example2.java +++ b/examples/src/main/java/de/learnlib/example/Example2.java @@ -35,7 +35,7 @@ import de.learnlib.oracle.membership.SULOracle; import de.learnlib.statistic.Statistics; import de.learnlib.sul.SUL; -import de.learnlib.util.Experiment.MealyExperiment; +import de.learnlib.util.MealyExperiment; import net.automatalib.automaton.transducer.MealyMachine; import net.automatalib.serialization.dot.GraphDOT; import net.automatalib.visualization.Visualization; diff --git a/examples/src/main/java/de/learnlib/example/bbc/LTSminExample1.java b/examples/src/main/java/de/learnlib/example/bbc/LTSminExample1.java index af7d85364..79dda6b92 100644 --- a/examples/src/main/java/de/learnlib/example/bbc/LTSminExample1.java +++ b/examples/src/main/java/de/learnlib/example/bbc/LTSminExample1.java @@ -36,7 +36,7 @@ import de.learnlib.oracle.property.DFALoggingPropertyOracle; import de.learnlib.testsupport.example.LearningExample.DFALearningExample; import de.learnlib.testsupport.example.dfa.ExampleTinyDFA; -import de.learnlib.util.Experiment.DFAExperiment; +import de.learnlib.util.DFAExperiment; import net.automatalib.alphabet.Alphabet; import net.automatalib.automaton.fsa.DFA; import net.automatalib.modelchecker.ltsmin.ltl.LTSminLTLDFABuilder; diff --git a/examples/src/main/java/de/learnlib/example/bbc/LTSminExample2.java b/examples/src/main/java/de/learnlib/example/bbc/LTSminExample2.java index 5eeea3673..cbf9d483d 100644 --- a/examples/src/main/java/de/learnlib/example/bbc/LTSminExample2.java +++ b/examples/src/main/java/de/learnlib/example/bbc/LTSminExample2.java @@ -36,7 +36,7 @@ import de.learnlib.oracle.property.MealyLoggingPropertyOracle; import de.learnlib.testsupport.example.LearningExample.MealyLearningExample; import de.learnlib.testsupport.example.mealy.ExampleTinyMealy; -import de.learnlib.util.Experiment.MealyExperiment; +import de.learnlib.util.MealyExperiment; import net.automatalib.alphabet.Alphabet; import net.automatalib.automaton.transducer.MealyMachine; import net.automatalib.modelchecker.ltsmin.ltl.LTSminLTLIOBuilder; diff --git a/examples/src/main/java/de/learnlib/example/bbc/LTSminExample3.java b/examples/src/main/java/de/learnlib/example/bbc/LTSminExample3.java index ea6c953f8..204a0e75d 100644 --- a/examples/src/main/java/de/learnlib/example/bbc/LTSminExample3.java +++ b/examples/src/main/java/de/learnlib/example/bbc/LTSminExample3.java @@ -36,7 +36,7 @@ import de.learnlib.oracle.property.MealyLoggingPropertyOracle; import de.learnlib.testsupport.example.LearningExample.MealyLearningExample; import de.learnlib.testsupport.example.mealy.ExampleTinyMealy; -import de.learnlib.util.Experiment.MealyExperiment; +import de.learnlib.util.MealyExperiment; import net.automatalib.alphabet.Alphabet; import net.automatalib.automaton.transducer.MealyMachine; import net.automatalib.modelchecker.ltsmin.ltl.LTSminLTLAlternatingBuilder; diff --git a/examples/src/main/java/de/learnlib/example/bbc/LTSminExample4.java b/examples/src/main/java/de/learnlib/example/bbc/LTSminExample4.java index 9984fb460..884db8bc6 100644 --- a/examples/src/main/java/de/learnlib/example/bbc/LTSminExample4.java +++ b/examples/src/main/java/de/learnlib/example/bbc/LTSminExample4.java @@ -40,7 +40,7 @@ import de.learnlib.oracle.property.DFAPropertyOracleChain; import de.learnlib.testsupport.example.LearningExample.DFALearningExample; import de.learnlib.testsupport.example.dfa.ExampleTinyDFA; -import de.learnlib.util.Experiment.DFAExperiment; +import de.learnlib.util.DFAExperiment; import net.automatalib.alphabet.Alphabet; import net.automatalib.automaton.fsa.DFA; import net.automatalib.modelchecker.ltsmin.LTSminUtil; diff --git a/examples/src/main/java/de/learnlib/example/resumable/ResumableExample.java b/examples/src/main/java/de/learnlib/example/resumable/ResumableExample.java index 2a767c5cf..8569aad43 100644 --- a/examples/src/main/java/de/learnlib/example/resumable/ResumableExample.java +++ b/examples/src/main/java/de/learnlib/example/resumable/ResumableExample.java @@ -35,9 +35,8 @@ import net.automatalib.alphabet.impl.GrowingMapAlphabet; import net.automatalib.automaton.fsa.impl.CompactDFA; import net.automatalib.util.automaton.random.RandomAutomata; -import org.apache.fury.Fury; -import org.apache.fury.logging.LogLevel; -import org.apache.fury.logging.LoggerFactory; +import org.apache.fory.Fory; +import org.apache.fory.logging.LoggerFactory; /** * An example to demonstrate the {@link Resumable} feature of LearnLib to continue learning setups from previously @@ -48,18 +47,22 @@ public final class ResumableExample { private static final CompactDFA TARGET; private static final Alphabet INITIAL_ALPHABET; - private static final Fury FURY; + private static final Fory FORY; static { LoggerFactory.useSlf4jLogging(true); - LoggerFactory.setLogLevel(LogLevel.ERROR_LEVEL); final int seed = 42; final int size = 100; TARGET = RandomAutomata.randomDFA(new Random(seed), size, Alphabets.characters('a', 'd')); INITIAL_ALPHABET = Alphabets.characters('a', 'b'); - FURY = Fury.builder().withRefTracking(true).requireClassRegistration(false).build(); + FORY = Fory.builder() + .requireClassRegistration(false) + .withCodegen(false) + .withRefTracking(true) + .withXlang(false) + .build(); } private ResumableExample() { @@ -114,12 +117,12 @@ private static void continueExploring(byte[] learnerData, byte[] cacheData, char } private static byte[] toBytes(Object state) { - return FURY.serialize(state); + return FORY.serialize(state); } @SuppressWarnings("unchecked") private static T fromBytes(byte[] bytes) { - return (T) FURY.deserialize(bytes); + return (T) FORY.deserialize(bytes); } private static void printStats(Setup setup) { diff --git a/examples/src/main/java/de/learnlib/example/sli/Example1.java b/examples/src/main/java/de/learnlib/example/sli/Example1.java index 5ba3dbc68..63fce0cf2 100644 --- a/examples/src/main/java/de/learnlib/example/sli/Example1.java +++ b/examples/src/main/java/de/learnlib/example/sli/Example1.java @@ -24,7 +24,7 @@ import de.learnlib.oracle.equivalence.mealy.StateLocalInputMealySimulatorEQOracle; import de.learnlib.oracle.membership.StateLocalInputSULOracle; import de.learnlib.sul.StateLocalInputSUL; -import de.learnlib.util.Experiment.MealyExperiment; +import de.learnlib.util.MealyExperiment; import net.automatalib.alphabet.Alphabet; import net.automatalib.alphabet.impl.Alphabets; import net.automatalib.automaton.transducer.MealyMachine; diff --git a/examples/src/main/java/de/learnlib/example/sli/Example2.java b/examples/src/main/java/de/learnlib/example/sli/Example2.java index 395e7449e..d6e78abb5 100644 --- a/examples/src/main/java/de/learnlib/example/sli/Example2.java +++ b/examples/src/main/java/de/learnlib/example/sli/Example2.java @@ -38,7 +38,7 @@ import de.learnlib.sul.SUL; import de.learnlib.sul.StateLocalInputSUL; import de.learnlib.testsupport.example.mealy.ExampleRandomStateLocalInputMealy; -import de.learnlib.util.Experiment.MealyExperiment; +import de.learnlib.util.MealyExperiment; import net.automatalib.alphabet.Alphabet; import net.automatalib.alphabet.impl.Alphabets; import net.automatalib.automaton.transducer.StateLocalInputMealyMachine; diff --git a/examples/src/main/java/module-info.java b/examples/src/main/java/module-info.java index e7c8cc0a3..4ac1f9409 100644 --- a/examples/src/main/java/module-info.java +++ b/examples/src/main/java/module-info.java @@ -54,7 +54,7 @@ requires net.automatalib.util; requires net.automatalib.serialization.dot; requires net.automatalib.visualization.dot; - requires org.apache.fury.core; + requires org.apache.fory.core; requires org.reactivestreams; requires reactor.core; diff --git a/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/EQOracleChain.java b/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/EQOracleChain.java index aa599ec77..7e7966824 100644 --- a/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/EQOracleChain.java +++ b/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/EQOracleChain.java @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; import de.learnlib.oracle.EquivalenceOracle; @@ -97,6 +98,10 @@ public void addOracle(EquivalenceOracle oracle) { oracles.add(oracle); } + public List> getOracles() { + return Collections.unmodifiableList(oracles); + } + @Override public @Nullable DefaultQuery findCounterExample(A hypothesis, Collection inputs) { for (EquivalenceOracle eqOracle : oracles) { diff --git a/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/vpa/RandomWellMatchedWordsEQOracle.java b/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/vpa/RandomWellMatchedWordsEQOracle.java index a99596fa3..34e295bd9 100644 --- a/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/vpa/RandomWellMatchedWordsEQOracle.java +++ b/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/vpa/RandomWellMatchedWordsEQOracle.java @@ -32,7 +32,7 @@ * @param * input symbol type */ -public final class RandomWellMatchedWordsEQOracle extends AbstractTestWordEQOracle, I, Boolean> { +public final class RandomWellMatchedWordsEQOracle extends AbstractTestWordEQOracle, I, D> { private final Random random; @@ -41,7 +41,7 @@ public final class RandomWellMatchedWordsEQOracle extends AbstractTestWordEQO private final int maxTests, minLength, maxLength; public RandomWellMatchedWordsEQOracle(Random random, - MembershipOracle oracle, + MembershipOracle oracle, double callProb, int maxTests, int minLength, @@ -50,7 +50,7 @@ public RandomWellMatchedWordsEQOracle(Random random, } public RandomWellMatchedWordsEQOracle(Random random, - MembershipOracle oracle, + MembershipOracle oracle, double callProb, int maxTests, int minLength, @@ -70,7 +70,7 @@ public RandomWellMatchedWordsEQOracle(Random random, } @Override - public Stream> generateTestWords(Output hypothesis, Collection inputs) { + public Stream> generateTestWords(Output hypothesis, Collection inputs) { if (!(inputs instanceof VPAlphabet)) { throw new IllegalArgumentException( diff --git a/oracles/membership-oracles/pom.xml b/oracles/membership-oracles/pom.xml index b97ddc306..b73312fd2 100644 --- a/oracles/membership-oracles/pom.xml +++ b/oracles/membership-oracles/pom.xml @@ -42,6 +42,10 @@ limitations under the License. net.automatalib automata-api + + net.automatalib + automata-commons-settings + net.automatalib automata-commons-util diff --git a/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/CLIOracle.java b/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/CLIOracle.java index 4840777d3..d00f304c8 100644 --- a/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/CLIOracle.java +++ b/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/CLIOracle.java @@ -17,7 +17,6 @@ import java.io.IOException; import java.util.List; -import java.util.Objects; import de.learnlib.oracle.SingleQueryOracle; import net.automatalib.common.util.process.ProcessUtil; @@ -32,12 +31,12 @@ * is determined based on the program's return code where {@code 0} indicates success and any other value indicates * failure. *

- * Queries are translated to program arguments (via the symbol's {@link #toString()} method). Depending on whether a + * Queries are translated to program arguments via the symbol's {@link Object#toString()} method. Depending on whether a * {@code reset} symbol has been specified, this oracle assumes either a stateless ({@code reset == null}) or stateful * ({@code reset != null}) communication. *

* In a stateless communication, all symbols of a query are passed to the program at once and invocations should be - * treated independently from each other. In a stateful communication, the program is executed multiple times with a + * treated independently of each other. In a stateful communication, the program is executed multiple times with a * single query symbol each, preceded by a single invocation with only the {@code reset} symbol. The exit code of the * last invocation determines the query response. * @@ -89,15 +88,18 @@ private boolean answerStatelessQuery(Word prefix, Word suffix) { this.commandLine.toArray(args); for (I p : prefix) { - args[idx++] = Objects.toString(p); + args[idx++] = String.valueOf(p); } for (I s : suffix) { - args[idx++] = Objects.toString(s); + args[idx++] = String.valueOf(s); } try { - return ProcessUtil.invokeProcess(args, LOGGER::debug, LOGGER::warn) == 0; + logInvocation(args); + final int exitCode = ProcessUtil.invokeProcess(args, LOGGER::debug, LOGGER::warn); + logResult(exitCode); + return exitCode == 0; } catch (IOException | InterruptedException e) { LOGGER.warn("Error while invoking process", e); return false; @@ -107,14 +109,23 @@ private boolean answerStatelessQuery(Word prefix, Word suffix) { @RequiresNonNull("this.reset") private boolean answerStatefulQuery(Word prefix, Word suffix) { try { - int returnCode = ProcessUtil.invokeProcess(toCommand(commandLine, reset), LOGGER::debug, LOGGER::warn); + final String[] resetCommand = toCommand(commandLine, reset); + logInvocation(resetCommand); + int returnCode = ProcessUtil.invokeProcess(resetCommand, LOGGER::debug, LOGGER::warn); + logResult(returnCode); for (I p : prefix) { - returnCode = ProcessUtil.invokeProcess(toCommand(commandLine, p), LOGGER::debug, LOGGER::warn); + final String[] command = toCommand(commandLine, p); + logInvocation(command); + returnCode = ProcessUtil.invokeProcess(command, LOGGER::debug, LOGGER::warn); + logResult(returnCode); } for (I s : suffix) { - returnCode = ProcessUtil.invokeProcess(toCommand(commandLine, s), LOGGER::debug, LOGGER::warn); + final String[] command = toCommand(commandLine, s); + logInvocation(command); + returnCode = ProcessUtil.invokeProcess(command, LOGGER::debug, LOGGER::warn); + logResult(returnCode); } return returnCode == 0; @@ -129,8 +140,16 @@ static String[] toCommand(List args, T arg) { final String[] result = new String[args.size() + 1]; args.toArray(result); - result[args.size()] = Objects.toString(arg); + result[args.size()] = String.valueOf(arg); return result; } + + private static void logInvocation(String[] command) { + LOGGER.debug("Invoking '{}'", (Object) command); + } + + private static void logResult(int exitCode) { + LOGGER.debug("Exit code '{}'", exitCode); + } } diff --git a/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/CLIOutputAdaptiveOracle.java b/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/CLIOutputAdaptiveOracle.java new file mode 100644 index 000000000..9c5a50103 --- /dev/null +++ b/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/CLIOutputAdaptiveOracle.java @@ -0,0 +1,110 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.oracle.membership; + +import java.io.IOException; +import java.io.StringReader; +import java.util.List; +import java.util.StringJoiner; +import java.util.function.Function; + +import de.learnlib.oracle.AdaptiveMembershipOracle; +import de.learnlib.oracle.SingleAdaptiveMembershipOracle; +import de.learnlib.query.AdaptiveQuery; +import de.learnlib.query.AdaptiveQuery.Response; +import net.automatalib.common.util.process.ProcessUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An oracle that delegates its queries to an external program via the command-line interface. Outputs of the queries + * are determined based on the provided output transformer. + *

+ * Queries are translated to program arguments via the symbol's {@link Object#toString()} method. Due to the nature of + * {@link AdaptiveMembershipOracle}s, communication is inherently stateful, i.e., the program is executed multiple times + * with a single query symbol each, preceded by a single invocation with only the {@code reset} symbol. + *

+ * The {@code outputTransformer} is used to transform responses of the individual invocations. + * + * @param + * input symbol type + * @param + * output symbol type + */ +public class CLIOutputAdaptiveOracle implements SingleAdaptiveMembershipOracle { + + private static final Logger LOGGER = LoggerFactory.getLogger(CLIOutputAdaptiveOracle.class); + + private final List commandLine; + private final Function outputTransformer; + private final String reset; + + /** + * Constructor. + * + * @param commandLine + * the command line, containing the main binary and potential additional arguments + * @param outputTransformer + * the transformer for the program's output. Receives the full (stdout) output of an individual invocation. + * @param reset + * the symbol passed to the program to indicate a reset + */ + public CLIOutputAdaptiveOracle(List commandLine, Function outputTransformer, String reset) { + this.commandLine = commandLine; + this.reset = reset; + this.outputTransformer = outputTransformer; + } + + @Override + public void processQuery(AdaptiveQuery query) { + try { + final String[] resetCommand = CLIOracle.toCommand(commandLine, reset); + logInvocation(resetCommand); + ProcessUtil.invokeProcess(resetCommand, LOGGER::debug, LOGGER::warn); + + Response response; + + do { + // ProcessUtil calls the stdout consumer for every line, so replicate the newlines in the output + final StringJoiner sj = new StringJoiner(System.lineSeparator()); + final I input = query.getInput(); + + final String[] command = CLIOracle.toCommand(commandLine, input); + + logInvocation(command); + ProcessUtil.invokeProcess(command, sj::add, LOGGER::warn); + logResult(sj); + + final O output = outputTransformer.apply(sj.toString()); + response = query.processOutput(output); + + if (response == Response.RESET) { + ProcessUtil.invokeProcess(commandLine, new StringReader(reset), LOGGER::debug, LOGGER::warn); + } + } while (response != Response.FINISHED); + } catch (IOException | InterruptedException e) { + throw new IllegalStateException(e); + } + } + + private static void logInvocation(String[] command) { + LOGGER.debug("Invoking '{}'", (Object) command); + } + + private static void logResult(StringJoiner output) { + LOGGER.debug("Received output '{}'", output); + } +} diff --git a/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/CLIOutputOracle.java b/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/CLIOutputOracle.java index c10d3e2b5..b50e14d42 100644 --- a/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/CLIOutputOracle.java +++ b/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/CLIOutputOracle.java @@ -19,7 +19,6 @@ import java.util.List; import java.util.Objects; import java.util.StringJoiner; -import java.util.function.BiFunction; import de.learnlib.oracle.SingleQueryOracle; import net.automatalib.common.util.process.ProcessUtil; @@ -33,13 +32,17 @@ * An oracle that delegates its queries to an external program via the command-line interface. Outputs of the queries * are determined based on the provided output transformer. *

- * Queries are translated to program arguments (via the symbol's {@link #toString()} method). Depending on whether a + * Queries are translated to program arguments via the symbol's {@link Object#toString()} method. Depending on whether a * {@code reset} symbol has been specified, this oracle assumes either a stateless ({@code reset == null}) or stateful * ({@code reset != null}) communication. *

* In a stateless communication, all symbols of a query are passed to the program at once and invocations should be - * treated independently from each other. In a stateful communication, the program is executed multiple times with a + * treated independently of each other. In a stateful communication, the program is executed multiple times with a * single query symbol each, preceded by a single invocation with only the {@code reset} symbol. + *

+ * With stateless communication, the {@code outputTransformer} receives the full program's output at once. With stateful + * communication, the individual invocation's outputs are concatenated via {@link System#lineSeparator()} before being + * passed to the transformer at once. * * @param * input symbol type @@ -51,7 +54,7 @@ public class CLIOutputOracle implements SingleQueryOracle { private static final Logger LOGGER = LoggerFactory.getLogger(CLIOutputOracle.class); private final List commandLine; - private final BiFunction outputTransformer; + private final OutputTransformer outputTransformer; private final @Nullable String reset; /** @@ -60,12 +63,12 @@ public class CLIOutputOracle implements SingleQueryOracle { * @param commandLine * the command line, containing the main binary and potential additional arguments * @param outputTransformer - * the transformer for the program's output. Receives the full process output (stdin and stderr) as well as - * the length of the query prefix for properly offsetting potentially {@link Word}-based output types. + * the transformer for the program's output. Receives the full (stdout) output as well as the length of the + * query prefix and suffix for properly offsetting potentially {@link Word}-based output types. * - * @see #CLIOutputOracle(List, BiFunction, String) + * @see #CLIOutputOracle(List, OutputTransformer, String) */ - public CLIOutputOracle(List commandLine, BiFunction outputTransformer) { + public CLIOutputOracle(List commandLine, OutputTransformer outputTransformer) { this(commandLine, outputTransformer, null); } @@ -75,14 +78,12 @@ public CLIOutputOracle(List commandLine, BiFunction * @param commandLine * the command line, containing the main binary and potential additional arguments * @param outputTransformer - * the transformer for the program's output. Receives the full process output (stdin and stderr) as well as - * the length of the query prefix for properly offsetting potentially {@link Word}-based output types. + * the transformer for the program's output. Receives the full (stdout) output as well as the length of the + * query prefix and suffix for properly offsetting potentially {@link Word}-based output types. * @param reset * the symbol passed to the program to indicate a reset */ - public CLIOutputOracle(List commandLine, - BiFunction outputTransformer, - @Nullable String reset) { + public CLIOutputOracle(List commandLine, OutputTransformer outputTransformer, @Nullable String reset) { this.commandLine = commandLine; this.reset = reset; this.outputTransformer = outputTransformer; @@ -108,11 +109,14 @@ private D answerStatelessQuery(Word prefix, Word suffix) { args[idx++] = Objects.toString(s); } + // ProcessUtil calls the stdout consumer for every line, so replicate the newlines in the output final StringJoiner sj = new StringJoiner(System.lineSeparator()); try { + logInvocation(args); ProcessUtil.invokeProcess(args, sj::add, LOGGER::warn); - return outputTransformer.apply(sj.toString(), prefix.length()); + logResult(sj); + return outputTransformer.transform(sj.toString(), prefix.length(), suffix.length()); } catch (IOException | InterruptedException e) { throw new IllegalStateException(e); } @@ -120,22 +124,65 @@ private D answerStatelessQuery(Word prefix, Word suffix) { @RequiresNonNull("this.reset") private D answerStatefulQuery(Word prefix, Word suffix) { + // ProcessUtil calls the stdout consumer for every line, so replicate the newlines in the output final StringJoiner sj = new StringJoiner(System.lineSeparator()); try { - ProcessUtil.invokeProcess(CLIOracle.toCommand(commandLine, reset), LOGGER::debug, LOGGER::warn); + final String[] resetCommand = CLIOracle.toCommand(commandLine, reset); + logInvocation(resetCommand); + ProcessUtil.invokeProcess(resetCommand, LOGGER::debug, LOGGER::warn); for (I p : prefix) { - ProcessUtil.invokeProcess(CLIOracle.toCommand(commandLine, p), sj::add, LOGGER::warn); + answerStatefulSymbol(p, sj); } for (I s : suffix) { - ProcessUtil.invokeProcess(CLIOracle.toCommand(commandLine, s), sj::add, LOGGER::warn); + answerStatefulSymbol(s, sj); } - return outputTransformer.apply(sj.toString(), prefix.length()); + return outputTransformer.transform(sj.toString(), prefix.length(), suffix.length()); } catch (IOException | InterruptedException e) { throw new IllegalStateException(e); } } + + private void answerStatefulSymbol(I i, StringJoiner sj) throws IOException, InterruptedException { + String[] command = CLIOracle.toCommand(commandLine, i); + logInvocation(command); + ProcessUtil.invokeProcess(command, sj::add, LOGGER::warn); + logResult(sj); + } + + private static void logInvocation(String[] command) { + LOGGER.debug("Invoking '{}'", (Object) command); + } + + private static void logResult(Object output) { + LOGGER.debug("Received output '{}'", output); + } + + /** + * Transformer for converting the {@link String}-based output of a CLI application to a custom-typed output. + * + * @param + * output domain type + */ + @FunctionalInterface + public interface OutputTransformer { + + /** + * Transforms the provided output to a custom output object. Additionally, receives information about the length + * of the original query's prefix and suffix (e.g., for {@link Word}-based outputs). + * + * @param output + * the stdout output of the invocation + * @param prefixLength + * the length of the query prefix + * @param suffixLength + * the length of the query suffix + * + * @return the output + */ + D transform(String output, int prefixLength, int suffixLength); + } } diff --git a/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/StdInOracle.java b/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/StdInOracle.java index fe512e641..88ab17e4e 100644 --- a/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/StdInOracle.java +++ b/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/StdInOracle.java @@ -18,9 +18,11 @@ import java.io.IOException; import java.io.StringReader; import java.util.List; -import java.util.Objects; +import java.util.StringJoiner; import de.learnlib.oracle.SingleQueryOracle; +import net.automatalib.common.setting.AutomataLibProperty; +import net.automatalib.common.setting.AutomataLibSettings; import net.automatalib.common.util.process.ProcessUtil; import net.automatalib.word.Word; import org.checkerframework.checker.nullness.qual.Nullable; @@ -33,13 +35,12 @@ * is determined based on the program's return code where {@code 0} indicates success and any other value indicates * failure. *

- * Queries are passed to the program's stdin stream (via the queries' {@link Word#toString()} method. You may adjust - * formatting via the available properties in AutomataLib's settings). Depending on whether a {@code reset} symbol has - * been specified, this oracle assumes either a stateless ({@code reset == null}) or stateful ({@code reset != null}) - * communication. + * Queries are passed to the program's stdin stream via the queries' {@link Object#toString()} method (separated by + * {@link AutomataLibProperty#WORD_SYMBOL_SEPARATOR}). Depending on whether a {@code reset} symbol has been specified, + * this oracle assumes either a stateless ({@code reset == null}) or stateful ({@code reset != null}) communication. *

* In a stateless communication, all symbols of a query are passed to the program at once and invocations should be - * treated independently from each other. In a stateful communication, the program is executed multiple times with a + * treated independently of each other. In a stateful communication, the program is executed multiple times with a * single query symbol each, preceded by a single invocation with only the {@code reset} symbol. The exit code of the * last invocation determines the query response. * @@ -49,6 +50,8 @@ public class StdInOracle implements SingleQueryOracle { private static final Logger LOGGER = LoggerFactory.getLogger(StdInOracle.class); + private static final String DELIMITER = + AutomataLibSettings.getInstance().getProperty(AutomataLibProperty.WORD_SYMBOL_SEPARATOR, " "); private final List commandLine; private final @Nullable String reset; @@ -84,11 +87,24 @@ public Boolean answerQuery(Word prefix, Word suffix) { } private boolean answerStatelessQuery(Word prefix, Word suffix) { + final StringJoiner sj = new StringJoiner(DELIMITER); + + for (I p : prefix) { + sj.add(String.valueOf(p)); + } + + for (I s : suffix) { + sj.add(String.valueOf(s)); + } + try { - return ProcessUtil.invokeProcess(commandLine, - new StringReader(prefix.concat(suffix).toString()), - LOGGER::debug, - LOGGER::warn) == 0; + logInvocation(commandLine, sj); + final int exitCode = ProcessUtil.invokeProcess(commandLine, + new StringReader(sj.toString()), + LOGGER::debug, + LOGGER::warn); + logResult(exitCode); + return exitCode == 0; } catch (IOException | InterruptedException e) { LOGGER.warn("Error while invoking process", e); return false; @@ -98,21 +114,27 @@ private boolean answerStatelessQuery(Word prefix, Word suffix) { @RequiresNonNull("this.reset") private boolean answerStatefulQuery(Word prefix, Word suffix) { try { + logInvocation(commandLine, reset); int returnCode = ProcessUtil.invokeProcess(commandLine, new StringReader(reset), LOGGER::debug, LOGGER::warn); + logResult(returnCode); for (I p : prefix) { + logInvocation(commandLine, p); returnCode = ProcessUtil.invokeProcess(commandLine, - new StringReader(Objects.toString(p)), + new StringReader(String.valueOf(p)), LOGGER::debug, LOGGER::warn); + logResult(returnCode); } for (I s : suffix) { + logInvocation(commandLine, s); returnCode = ProcessUtil.invokeProcess(commandLine, - new StringReader(Objects.toString(s)), + new StringReader(String.valueOf(s)), LOGGER::debug, LOGGER::warn); + logResult(returnCode); } return returnCode == 0; @@ -121,4 +143,12 @@ private boolean answerStatefulQuery(Word prefix, Word suffix) { return false; } } + + private static void logInvocation(List command, Object payload) { + LOGGER.debug("Invoking '{}' with payload '{}'", command, payload); + } + + private static void logResult(int exitCode) { + LOGGER.debug("Exit code '{}'", exitCode); + } } diff --git a/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/StdInOutputAdaptiveOracle.java b/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/StdInOutputAdaptiveOracle.java new file mode 100644 index 000000000..cb7455948 --- /dev/null +++ b/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/StdInOutputAdaptiveOracle.java @@ -0,0 +1,111 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.oracle.membership; + +import java.io.IOException; +import java.io.StringReader; +import java.util.List; +import java.util.Objects; +import java.util.StringJoiner; +import java.util.function.Function; + +import de.learnlib.oracle.AdaptiveMembershipOracle; +import de.learnlib.oracle.SingleAdaptiveMembershipOracle; +import de.learnlib.query.AdaptiveQuery; +import de.learnlib.query.AdaptiveQuery.Response; +import net.automatalib.common.util.process.ProcessUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An oracle that delegates its queries to an external program via the command-line interface. Outputs of the queries + * are determined based on the provided output transformer. + *

+ * Queries are passed to the program's stdin stream via the symbols' {@link Object#toString()} method. Due to the nature + * of {@link AdaptiveMembershipOracle}s, communication is inherently stateful, i.e., the program is executed multiple + * times with a single query symbol each, preceded by a single invocation with only the {@code reset} symbol. + *

+ * The {@code outputTransformer} is used to transform responses of the individual invocations. + * + * @param + * input symbol type + * @param + * output symbol type + */ +public class StdInOutputAdaptiveOracle implements SingleAdaptiveMembershipOracle { + + private static final Logger LOGGER = LoggerFactory.getLogger(StdInOutputAdaptiveOracle.class); + + private final List commandLine; + private final Function outputTransformer; + private final String reset; + + /** + * Constructor. + * + * @param commandLine + * the command line, containing the main binary and potential additional arguments + * @param outputTransformer + * the transformer for the program's output. Receives the full (stdout) output of an individual invocation. + * @param reset + * the symbol passed to the program to indicate a reset + */ + public StdInOutputAdaptiveOracle(List commandLine, Function outputTransformer, String reset) { + this.commandLine = commandLine; + this.outputTransformer = outputTransformer; + this.reset = reset; + } + + @Override + public void processQuery(AdaptiveQuery query) { + try { + logInvocation(commandLine, reset); + ProcessUtil.invokeProcess(commandLine, new StringReader(reset), LOGGER::debug, LOGGER::warn); + + Response response; + + do { + // ProcessUtil calls the stdout consumer for every line, so replicate the newlines in the output + final StringJoiner sj = new StringJoiner(System.lineSeparator()); + final I input = query.getInput(); + + logInvocation(commandLine, input); + ProcessUtil.invokeProcess(commandLine, + new StringReader(Objects.toString(input)), + sj::add, + LOGGER::warn); + logResult(sj); + + final O output = outputTransformer.apply(sj.toString()); + response = query.processOutput(output); + + if (response == Response.RESET) { + ProcessUtil.invokeProcess(commandLine, new StringReader(reset), LOGGER::debug, LOGGER::warn); + } + } while (response != Response.FINISHED); + } catch (IOException | InterruptedException e) { + throw new IllegalStateException(e); + } + } + + private static void logInvocation(List command, Object payload) { + LOGGER.debug("Invoking '{}' with payload '{}'", command, payload); + } + + private static void logResult(StringJoiner output) { + LOGGER.debug("Received output '{}'", output); + } +} diff --git a/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/StdInOutputOracle.java b/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/StdInOutputOracle.java index feca9da52..c4ca03957 100644 --- a/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/StdInOutputOracle.java +++ b/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/StdInOutputOracle.java @@ -18,11 +18,11 @@ import java.io.IOException; import java.io.StringReader; import java.util.List; -import java.util.Objects; import java.util.StringJoiner; -import java.util.function.BiFunction; import de.learnlib.oracle.SingleQueryOracle; +import net.automatalib.common.setting.AutomataLibProperty; +import net.automatalib.common.setting.AutomataLibSettings; import net.automatalib.common.util.process.ProcessUtil; import net.automatalib.word.Word; import org.checkerframework.checker.nullness.qual.Nullable; @@ -34,15 +34,17 @@ * An oracle that delegates its queries to an external program via the command-line interface. Outputs of the queries * are determined based on the provided output transformer. *

- * Queries are passed to the program's stdin stream (via the queries' {@link Word#toString()} method. You may adjust - * formatting via the available properties in AutomataLib's settings). Depending on whether a {@code reset} symbol has - * been specified, this oracle assumes either a stateless ({@code reset == null}) or stateful ({@code reset != null}) - * communication. + * Queries are passed to the program's stdin stream via the symbols' {@link Object#toString()} method (separated by + * {@link AutomataLibProperty#WORD_SYMBOL_SEPARATOR}). Depending on whether a {@code reset} symbol has been specified, + * this oracle assumes either a stateless ({@code reset == null}) or stateful ({@code reset != null}) communication. *

* In a stateless communication, all symbols of a query are passed to the program at once and invocations should be - * treated independently from each other. In a stateful communication, the program is executed multiple times with a - * single query symbol each, preceded by a single invocation with only the {@code reset} symbol. The exit code of the - * last invocation determines the query response. + * treated independently of each other. In a stateful communication, the program is executed multiple times with a + * single query symbol each, preceded by a single invocation with only the {@code reset} symbol. + *

+ * With stateless communication, the {@code outputTransformer} receives the full program's output at once. With stateful + * communication, the individual invocation's outputs are concatenated via {@link System#lineSeparator()} before being + * passed to the transformer at once. * * @param * input symbol type @@ -52,9 +54,11 @@ public class StdInOutputOracle implements SingleQueryOracle { private static final Logger LOGGER = LoggerFactory.getLogger(StdInOutputOracle.class); + private static final String DELIMITER = + AutomataLibSettings.getInstance().getProperty(AutomataLibProperty.WORD_SYMBOL_SEPARATOR, " "); private final List commandLine; - private final BiFunction outputTransformer; + private final OutputTransformer outputTransformer; private final @Nullable String reset; /** @@ -64,11 +68,11 @@ public class StdInOutputOracle implements SingleQueryOracle { * the command line, containing the main binary and potential additional arguments * @param outputTransformer * the transformer for the program's output. Receives the full (stdout) output as well as the length of the - * query prefix for properly offsetting potentially {@link Word}-based output types. + * query prefix and suffix for properly offsetting potentially {@link Word}-based output types. * - * @see #StdInOutputOracle(List, BiFunction, String) + * @see #StdInOutputOracle(List, OutputTransformer, String) */ - public StdInOutputOracle(List commandLine, BiFunction outputTransformer) { + public StdInOutputOracle(List commandLine, OutputTransformer outputTransformer) { this(commandLine, outputTransformer, null); } @@ -79,13 +83,11 @@ public StdInOutputOracle(List commandLine, BiFunction commandLine, - BiFunction outputTransformer, - @Nullable String reset) { + public StdInOutputOracle(List commandLine, OutputTransformer outputTransformer, @Nullable String reset) { this.commandLine = commandLine; this.outputTransformer = outputTransformer; this.reset = reset; @@ -97,14 +99,25 @@ public D answerQuery(Word prefix, Word suffix) { } private D answerStatelessQuery(Word prefix, Word suffix) { - final StringJoiner sj = new StringJoiner(System.lineSeparator()); + final StringJoiner input = new StringJoiner(DELIMITER); + + for (I p : prefix) { + input.add(String.valueOf(p)); + } + + for (I s : suffix) { + input.add(String.valueOf(s)); + } try { - ProcessUtil.invokeProcess(commandLine, - new StringReader(prefix.concat(suffix).toString()), - sj::add, - LOGGER::warn); - return outputTransformer.apply(sj.toString(), prefix.length()); + // ProcessUtil calls the stdout consumer for every line, so replicate the newlines in the output + final StringJoiner sj = new StringJoiner(System.lineSeparator()); + + logInvocation(commandLine, input); + ProcessUtil.invokeProcess(commandLine, new StringReader(input.toString()), sj::add, LOGGER::warn); + logResult(sj); + + return outputTransformer.transform(sj.toString(), prefix.length(), suffix.length()); } catch (IOException | InterruptedException e) { throw new IllegalStateException(e); } @@ -112,22 +125,63 @@ private D answerStatelessQuery(Word prefix, Word suffix) { @RequiresNonNull("this.reset") private D answerStatefulQuery(Word prefix, Word suffix) { + // ProcessUtil calls the stdout consumer for every line, so replicate the newlines in the output final StringJoiner sj = new StringJoiner(System.lineSeparator()); try { + logInvocation(commandLine, reset); ProcessUtil.invokeProcess(commandLine, new StringReader(reset), LOGGER::debug, LOGGER::warn); for (I p : prefix) { - ProcessUtil.invokeProcess(commandLine, new StringReader(Objects.toString(p)), sj::add, LOGGER::warn); + answerStatefulSymbol(p, sj); } for (I s : suffix) { - ProcessUtil.invokeProcess(commandLine, new StringReader(Objects.toString(s)), sj::add, LOGGER::warn); + answerStatefulSymbol(s, sj); } - return outputTransformer.apply(sj.toString(), prefix.length()); + return outputTransformer.transform(sj.toString(), prefix.length(), suffix.length()); } catch (IOException | InterruptedException e) { throw new IllegalStateException(e); } } + + private void answerStatefulSymbol(I i, StringJoiner sj) throws IOException, InterruptedException { + logInvocation(commandLine, i); + ProcessUtil.invokeProcess(commandLine, new StringReader(String.valueOf(i)), sj::add, LOGGER::warn); + logResult(sj); + } + + private static void logInvocation(List command, Object payload) { + LOGGER.debug("Invoking '{}' with payload '{}'", command, payload); + } + + private static void logResult(Object output) { + LOGGER.debug("Received output '{}'", output); + } + + /** + * Transformer for converting the {@link String}-based output of a CLI application to a custom-typed output. + * + * @param + * output domain type + */ + @FunctionalInterface + public interface OutputTransformer { + + /** + * Transforms the provided output to a custom output object. Additionally, receives information about the length + * of the original query's prefix and suffix (e.g., for {@link Word}-based outputs). + * + * @param output + * the stdout output of the invocation + * @param prefixLength + * the length of the query prefix + * @param suffixLength + * the length of the query suffix + * + * @return the output + */ + D transform(String output, int prefixLength, int suffixLength); + } } diff --git a/oracles/membership-oracles/src/main/java/module-info.java b/oracles/membership-oracles/src/main/java/module-info.java index a41940b9a..78c65c152 100644 --- a/oracles/membership-oracles/src/main/java/module-info.java +++ b/oracles/membership-oracles/src/main/java/module-info.java @@ -30,6 +30,7 @@ requires de.learnlib.api; requires net.automatalib.api; + requires net.automatalib.common.setting; requires net.automatalib.common.util; requires org.slf4j; diff --git a/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/AbstractPythonTest.java b/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/AbstractPythonTest.java index 7c9e4c86f..387fe516f 100644 --- a/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/AbstractPythonTest.java +++ b/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/AbstractPythonTest.java @@ -27,7 +27,7 @@ public abstract class AbstractPythonTest { - protected static final String PROGRAM = "python"; + protected static final String PROGRAM = "python3"; protected static String getPathToScript(String script) throws URISyntaxException { final URL resource = Objects.requireNonNull(AbstractPythonTest.class.getResource(script)); @@ -37,11 +37,11 @@ protected static String getPathToScript(String script) throws URISyntaxException @BeforeTest public void setUp() { try { - if (ProcessUtil.invokeProcess(new String[] {"python", "--version"}) != 0) { - throw new SkipException("python not supported"); + if (ProcessUtil.invokeProcess(new String[] {PROGRAM, "--version"}) != 0) { + throw new SkipException("python3 not supported"); } } catch (IOException | InterruptedException e) { - throw new SkipException("python not supported"); + throw new SkipException("python3 not supported"); } } diff --git a/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/CLIOracleTest.java b/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/CLIOracleTest.java index cd34cf68e..3bd0c5f2a 100644 --- a/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/CLIOracleTest.java +++ b/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/CLIOracleTest.java @@ -29,12 +29,14 @@ public void testStatelessCommunication() throws URISyntaxException { final String script = getPathToScript("/stateless_sul.py"); final CLIOracle oracle = new CLIOracle<>(Arrays.asList(PROGRAM, script)); + Assert.assertEquals(oracle.answerQuery(Word.epsilon()), false); Assert.assertEquals(oracle.answerQuery(Word.epsilon(), Word.fromString("ab")), false); Assert.assertEquals(oracle.answerQuery(Word.fromLetter('a'), Word.fromString("ab")), true); final String brokenScript = script.substring(0, script.length() - 3) + "2.py"; final CLIOracle brokenOracle = new CLIOracle<>(Arrays.asList(PROGRAM, brokenScript)); + Assert.assertEquals(oracle.answerQuery(Word.epsilon()), false); Assert.assertEquals(brokenOracle.answerQuery(Word.epsilon(), Word.fromString("ab")), false); Assert.assertEquals(brokenOracle.answerQuery(Word.fromLetter('a'), Word.fromString("ab")), false); } @@ -45,12 +47,14 @@ public void testStatefulCommunication() throws URISyntaxException { final String reset = "reset"; final CLIOracle oracle = new CLIOracle<>(Arrays.asList(PROGRAM, script), reset); + Assert.assertEquals(oracle.answerQuery(Word.epsilon()), false); Assert.assertEquals(oracle.answerQuery(Word.epsilon(), Word.fromString("ab")), false); Assert.assertEquals(oracle.answerQuery(Word.fromLetter('a'), Word.fromString("ab")), true); final String brokenScript = script.substring(0, script.length() - 3) + "2.py"; final CLIOracle brokenOracle = new CLIOracle<>(Arrays.asList(PROGRAM, brokenScript), reset); + Assert.assertEquals(oracle.answerQuery(Word.epsilon()), false); Assert.assertEquals(brokenOracle.answerQuery(Word.epsilon(), Word.fromString("ab")), false); Assert.assertEquals(brokenOracle.answerQuery(Word.fromLetter('a'), Word.fromString("ab")), false); } diff --git a/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/CLIOutputAdaptiveOracleTest.java b/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/CLIOutputAdaptiveOracleTest.java new file mode 100644 index 000000000..488351911 --- /dev/null +++ b/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/CLIOutputAdaptiveOracleTest.java @@ -0,0 +1,48 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.oracle.membership; + +import java.net.URISyntaxException; +import java.util.Arrays; + +import net.automatalib.word.Word; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class CLIOutputAdaptiveOracleTest extends AbstractPythonTest { + + @Test + public void testStatefulCommunication() throws URISyntaxException { + final String script = getPathToScript("/stateful_sul.py"); + final String reset = "reset"; + final CLIOutputAdaptiveOracle oracle = + new CLIOutputAdaptiveOracle<>(Arrays.asList(PROGRAM, script), Integer::parseInt, reset); + + final AdaptiveTestQuery q1 = new AdaptiveTestQuery<>(Word.fromString("ab")); + final AdaptiveTestQuery q2 = + new AdaptiveTestQuery<>(Word.fromLetter('a'), Word.fromString("ab")); + + oracle.processQuery(q1); + oracle.processQuery(q2); + + Assert.assertEquals(q1.getOutputs().size(), 1); + Assert.assertEquals(q1.getOutputs().get(0).toWord(), Word.fromSymbols(97, 98)); + Assert.assertEquals(q2.getOutputs().size(), 2); + Assert.assertEquals(q2.getOutputs().get(0).toWord(), Word.fromLetter(97)); + Assert.assertEquals(q2.getOutputs().get(1).toWord(), Word.fromSymbols(97, 98)); + } + +} diff --git a/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/CLIOutputOracleTest.java b/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/CLIOutputOracleTest.java index 3c8f0299a..c9fcff01d 100644 --- a/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/CLIOutputOracleTest.java +++ b/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/CLIOutputOracleTest.java @@ -28,14 +28,15 @@ public class CLIOutputOracleTest extends AbstractPythonTest { public void testStatelessCommunication() throws URISyntaxException { final String script = getPathToScript("/stateless_sul.py"); final CLIOutputOracle> oracle = - new CLIOutputOracle<>(Arrays.asList(PROGRAM, script), this::parseOutput); + new CLIOutputOracle<>(Arrays.asList(PROGRAM, script), CLIOutputOracleTest::parseOutput); + Assert.assertEquals(oracle.answerQuery(Word.epsilon()), Word.epsilon()); Assert.assertEquals(oracle.answerQuery(Word.epsilon(), Word.fromString("ab")), Word.fromSymbols(97, 98)); Assert.assertEquals(oracle.answerQuery(Word.fromLetter('a'), Word.fromString("ab")), Word.fromSymbols(97, 98)); final String brokenScript = script.substring(0, script.length() - 3) + "2.py"; final CLIOutputOracle> brokenOracle = - new CLIOutputOracle<>(Arrays.asList(PROGRAM, brokenScript), this::parseOutput); + new CLIOutputOracle<>(Arrays.asList(PROGRAM, brokenScript), CLIOutputOracleTest::parseOutput); Assert.assertThrows(() -> brokenOracle.answerQuery(Word.epsilon(), Word.fromString("ab"))); Assert.assertThrows(() -> brokenOracle.answerQuery(Word.fromLetter('a'), Word.fromString("ab"))); @@ -46,23 +47,36 @@ public void testStatefulCommunication() throws URISyntaxException { final String script = getPathToScript("/stateful_sul.py"); final String reset = "reset"; final CLIOutputOracle> oracle = - new CLIOutputOracle<>(Arrays.asList(PROGRAM, script), this::parseOutput, reset); + new CLIOutputOracle<>(Arrays.asList(PROGRAM, script), CLIOutputOracleTest::parseOutput, reset); + Assert.assertEquals(oracle.answerQuery(Word.epsilon()), Word.epsilon()); Assert.assertEquals(oracle.answerQuery(Word.epsilon(), Word.fromString("ab")), Word.fromSymbols(97, 98)); Assert.assertEquals(oracle.answerQuery(Word.fromLetter('a'), Word.fromString("ab")), Word.fromSymbols(97, 98)); final String brokenScript = script.substring(0, script.length() - 3) + "2.py"; final CLIOutputOracle> brokenOracle = - new CLIOutputOracle<>(Arrays.asList(PROGRAM, brokenScript), this::parseOutput, reset); + new CLIOutputOracle<>(Arrays.asList(PROGRAM, brokenScript), CLIOutputOracleTest::parseOutput, reset); Assert.assertThrows(() -> brokenOracle.answerQuery(Word.epsilon(), Word.fromString("ab"))); Assert.assertThrows(() -> brokenOracle.answerQuery(Word.fromLetter('a'), Word.fromString("ab"))); } - private Word parseOutput(String input, Integer offset) { - return Arrays.stream(input.split(System.lineSeparator())) - .map(Integer::parseInt) - .skip(offset) - .collect(Word.collector()); + static Word parseOutput(String input, int prefix, int suffix) { + if (suffix == 0) { + return Word.epsilon(); + } + + if (input.isBlank()) { + throw new IllegalStateException(); + } + + final Word result = + Arrays.stream(input.split(System.lineSeparator())).map(Integer::parseInt).collect(Word.collector()); + + if (result.length() != (prefix + suffix)) { + throw new IllegalStateException(); + } + + return result.suffix(suffix); } } diff --git a/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/StdInOracleTest.java b/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/StdInOracleTest.java index d289df1fb..474e9bcdd 100644 --- a/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/StdInOracleTest.java +++ b/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/StdInOracleTest.java @@ -26,9 +26,10 @@ public class StdInOracleTest extends AbstractPythonTest { @Test public void testStatelessCommunication() throws URISyntaxException { - final String script = getPathToScript("/stateless_sul.py"); + final String script = getPathToScript("/stateless_stdin_sul.py"); final StdInOracle oracle = new StdInOracle<>(Arrays.asList(PROGRAM, script)); + Assert.assertEquals(oracle.answerQuery(Word.epsilon()), false); Assert.assertEquals(oracle.answerQuery(Word.epsilon(), Word.fromString("ab")), false); Assert.assertEquals(oracle.answerQuery(Word.fromLetter('a'), Word.fromString("ab")), true); @@ -41,10 +42,11 @@ public void testStatelessCommunication() throws URISyntaxException { @Test public void testStatefulCommunication() throws URISyntaxException { - final String script = getPathToScript("/stateful_sul.py"); + final String script = getPathToScript("/stateful_stdin_sul.py"); final String reset = "reset"; final StdInOracle oracle = new StdInOracle<>(Arrays.asList(PROGRAM, script), reset); + Assert.assertEquals(oracle.answerQuery(Word.epsilon()), false); Assert.assertEquals(oracle.answerQuery(Word.epsilon(), Word.fromString("ab")), false); Assert.assertEquals(oracle.answerQuery(Word.fromLetter('a'), Word.fromString("ab")), true); diff --git a/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/StdInOutputAdaptiveOracleTest.java b/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/StdInOutputAdaptiveOracleTest.java new file mode 100644 index 000000000..6151e4e18 --- /dev/null +++ b/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/StdInOutputAdaptiveOracleTest.java @@ -0,0 +1,59 @@ +/* Copyright (C) 2013-2026 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.learnlib.oracle.membership; + +import java.net.URISyntaxException; +import java.util.Arrays; + +import net.automatalib.word.Word; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class StdInOutputAdaptiveOracleTest extends AbstractPythonTest { + + @Test + public void testStatefulCommunication() throws URISyntaxException { + final String script = getPathToScript("/stateful_stdin_sul.py"); + final String reset = "reset"; + final StdInOutputAdaptiveOracle oracle = + new StdInOutputAdaptiveOracle<>(Arrays.asList(PROGRAM, script), Integer::parseInt, reset); + + final AdaptiveTestQuery q1 = new AdaptiveTestQuery<>(Word.fromString("ab")); + final AdaptiveTestQuery q2 = + new AdaptiveTestQuery<>(Word.fromLetter('a'), Word.fromString("ab")); + + oracle.processQuery(q1); + oracle.processQuery(q2); + + Assert.assertEquals(q1.getOutputs().size(), 1); + Assert.assertEquals(q1.getOutputs().get(0).toWord(), Word.fromSymbols(97, 98)); + Assert.assertEquals(q2.getOutputs().size(), 2); + Assert.assertEquals(q2.getOutputs().get(0).toWord(), Word.fromLetter(97)); + Assert.assertEquals(q2.getOutputs().get(1).toWord(), Word.fromSymbols(97, 98)); + + final String brokenScript = script.substring(0, script.length() - 3) + "2.py"; + final StdInOutputAdaptiveOracle brokenOracle = + new StdInOutputAdaptiveOracle<>(Arrays.asList(PROGRAM, brokenScript), Integer::parseInt, reset); + + final AdaptiveTestQuery bq1 = new AdaptiveTestQuery<>(Word.fromString("ab")); + final AdaptiveTestQuery bq2 = + new AdaptiveTestQuery<>(Word.fromLetter('a'), Word.fromString("ab")); + + Assert.assertThrows(() -> brokenOracle.processQuery(bq1)); + Assert.assertThrows(() -> brokenOracle.processQuery(bq2)); + } + +} diff --git a/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/StdInOutputOracleTest.java b/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/StdInOutputOracleTest.java index 88f66c948..47c2f6291 100644 --- a/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/StdInOutputOracleTest.java +++ b/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/StdInOutputOracleTest.java @@ -26,16 +26,17 @@ public class StdInOutputOracleTest extends AbstractPythonTest { @Test public void testStatelessCommunication() throws URISyntaxException { - final String script = getPathToScript("/stateless_sul.py"); + final String script = getPathToScript("/stateless_stdin_sul.py"); final StdInOutputOracle> oracle = - new StdInOutputOracle<>(Arrays.asList(PROGRAM, script), this::parseOutput); + new StdInOutputOracle<>(Arrays.asList(PROGRAM, script), CLIOutputOracleTest::parseOutput); + Assert.assertEquals(oracle.answerQuery(Word.epsilon()), Word.epsilon()); Assert.assertEquals(oracle.answerQuery(Word.epsilon(), Word.fromString("ab")), Word.fromSymbols(97, 98)); Assert.assertEquals(oracle.answerQuery(Word.fromLetter('a'), Word.fromString("ab")), Word.fromSymbols(97, 98)); final String brokenScript = script.substring(0, script.length() - 3) + "2.py"; final StdInOutputOracle> brokenOracle = - new StdInOutputOracle<>(Arrays.asList(PROGRAM, brokenScript), this::parseOutput); + new StdInOutputOracle<>(Arrays.asList(PROGRAM, brokenScript), CLIOutputOracleTest::parseOutput); Assert.assertThrows(() -> brokenOracle.answerQuery(Word.epsilon(), Word.fromString("ab"))); Assert.assertThrows(() -> brokenOracle.answerQuery(Word.fromLetter('a'), Word.fromString("ab"))); @@ -43,27 +44,21 @@ public void testStatelessCommunication() throws URISyntaxException { @Test public void testStatefulCommunication() throws URISyntaxException { - final String script = getPathToScript("/stateful_sul.py"); + final String script = getPathToScript("/stateful_stdin_sul.py"); final String reset = "reset"; final StdInOutputOracle> oracle = - new StdInOutputOracle<>(Arrays.asList(PROGRAM, script), this::parseOutput, reset); + new StdInOutputOracle<>(Arrays.asList(PROGRAM, script), CLIOutputOracleTest::parseOutput, reset); + Assert.assertEquals(oracle.answerQuery(Word.epsilon()), Word.epsilon()); Assert.assertEquals(oracle.answerQuery(Word.epsilon(), Word.fromString("ab")), Word.fromSymbols(97, 98)); Assert.assertEquals(oracle.answerQuery(Word.fromLetter('a'), Word.fromString("ab")), Word.fromSymbols(97, 98)); final String brokenScript = script.substring(0, script.length() - 3) + "2.py"; final StdInOutputOracle> brokenOracle = - new StdInOutputOracle<>(Arrays.asList(PROGRAM, brokenScript), this::parseOutput, reset); + new StdInOutputOracle<>(Arrays.asList(PROGRAM, brokenScript), CLIOutputOracleTest::parseOutput, reset); Assert.assertThrows(() -> brokenOracle.answerQuery(Word.epsilon(), Word.fromString("ab"))); Assert.assertThrows(() -> brokenOracle.answerQuery(Word.fromLetter('a'), Word.fromString("ab"))); } - private Word parseOutput(String input, Integer offset) { - return Arrays.stream(input.split(System.lineSeparator())) - .map(Integer::parseInt) - .skip(offset) - .collect(Word.collector()); - } - } diff --git a/oracles/membership-oracles/src/test/resources/stateful_stdin_sul.py b/oracles/membership-oracles/src/test/resources/stateful_stdin_sul.py new file mode 100755 index 000000000..8d926547e --- /dev/null +++ b/oracles/membership-oracles/src/test/resources/stateful_stdin_sul.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +from pathlib import Path + +import sys +import pickle +import os.path + +def main(): + folder = Path(sys.argv[0]).parent + path = os.path.join(folder, "state.p") + argv = 1 + + if os.path.isfile(path): + argv = pickle.load(open(path, "rb")) + + for line in sys.stdin: + for arg in line.split(): + if arg == "reset": + argv = 1 + else: + argv += 1 + print(sum([ord(c) for c in arg])) + print(arg, file=sys.stderr) + + pickle.dump(argv, open(path, "wb")) + sys.exit(argv % 2) + +if __name__ == "__main__": + main() diff --git a/oracles/membership-oracles/src/test/resources/stateful_sul.py b/oracles/membership-oracles/src/test/resources/stateful_sul.py index d3c7d1b56..7fdb59835 100755 --- a/oracles/membership-oracles/src/test/resources/stateful_sul.py +++ b/oracles/membership-oracles/src/test/resources/stateful_sul.py @@ -1,4 +1,4 @@ -#!/bin/python +#!/usr/bin/env python3 from pathlib import Path import sys @@ -13,23 +13,13 @@ def main(): if os.path.isfile(path): argv = pickle.load(open(path, "rb")) - if len(sys.argv) > 1: # arg mode - for arg in sys.argv[1:]: - if arg == "reset": - argv = 1 - else: - argv += 1 - print(sum([ord(c) for c in arg])) - print(arg, file=sys.stderr) - else: # stdin mode - for line in sys.stdin: - for arg in line.split(): - if arg == "reset": - argv = 1 - else: - argv += 1 - print(sum([ord(c) for c in arg])) - print(arg, file=sys.stderr) + for arg in sys.argv[1:]: + if arg == "reset": + argv = 1 + else: + argv += 1 + print(sum([ord(c) for c in arg])) + print(arg, file=sys.stderr) pickle.dump(argv, open(path, "wb")) sys.exit(argv % 2) diff --git a/oracles/membership-oracles/src/test/resources/stateless_stdin_sul.py b/oracles/membership-oracles/src/test/resources/stateless_stdin_sul.py new file mode 100755 index 000000000..3cb1381d2 --- /dev/null +++ b/oracles/membership-oracles/src/test/resources/stateless_stdin_sul.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +import sys + +def main(): + argv = len(sys.argv) + + for line in sys.stdin: + for arg in line.split(): + argv += 1 + print(sum([ord(c) for c in arg])) + print(arg, file=sys.stderr) + + sys.exit(argv % 2) + +if __name__ == "__main__": + main() diff --git a/oracles/membership-oracles/src/test/resources/stateless_sul.py b/oracles/membership-oracles/src/test/resources/stateless_sul.py index 75e55537f..31b54ccb9 100755 --- a/oracles/membership-oracles/src/test/resources/stateless_sul.py +++ b/oracles/membership-oracles/src/test/resources/stateless_sul.py @@ -1,19 +1,12 @@ -#!/bin/python +#!/usr/bin/env python3 import sys def main(): argv = len(sys.argv) - if argv > 1: # arg mode - for arg in sys.argv[1:]: - print(sum([ord(c) for c in arg])) - print(arg, file=sys.stderr) - else: # stdin mode - for line in sys.stdin: - for arg in line.split(): - argv += 1 - print(sum([ord(c) for c in arg])) - print(arg, file=sys.stderr) + for arg in sys.argv[1:]: + print(sum([ord(c) for c in arg])) + print(arg, file=sys.stderr) sys.exit(argv % 2) diff --git a/pom.xml b/pom.xml index 8173c29c2..96b1a73c2 100644 --- a/pom.xml +++ b/pom.xml @@ -256,7 +256,10 @@ limitations under the License. 0.8.15 3.5.0 3.12.0 + 3.3.0 + 2.7.1 1.0.0 + 1.7.1 3.28.0 3.3.1 3.5.0 @@ -274,12 +277,13 @@ limitations under the License. 4.2.1 12.3.0 1.10.0 - 0.9.0 + 1.4.0 2.3.1 1.5.35 1.11 5.22.0 3.3.0 + 4.7.7 7.26.0 1.0.4 3.8.6 @@ -627,7 +631,7 @@ limitations under the License. test - + org.checkerframework checker-qual @@ -642,12 +646,6 @@ limitations under the License. provided - - org.apache.fury - fury-core - ${fury.version} - - org.kohsuke.metainf-services metainf-services @@ -655,6 +653,14 @@ limitations under the License. provided + + + org.apache.fory + fory-core + ${fory.version} + + + org.slf4j slf4j-api @@ -695,6 +701,12 @@ limitations under the License. ${reactive-streams.version} + + + info.picocli + picocli + ${picocli.version} + @@ -791,14 +803,6 @@ limitations under the License. true - - - - integration-test - verify - - - org.jacoco @@ -846,7 +850,7 @@ limitations under the License. integration-tests,code-analysis deploy site site:stage scm-publish:publish-scm - release,bundles,!examples,!integration-tests,!code-analysis + release,bundles,!examples,!cli,!integration-tests,!code-analysis @@ -921,6 +925,9 @@ limitations under the License. org.apache.maven.plugins maven-resources-plugin ${resources-plugin.version} + + ${project.build.sourceEncoding} + org.apache.maven.plugins @@ -946,6 +953,11 @@ limitations under the License. maven-jar-plugin ${jar-plugin.version} + + org.apache.maven.plugins + maven-jlink-plugin + ${jlink-plugin.version} + org.apache.maven.plugins maven-install-plugin @@ -956,6 +968,11 @@ limitations under the License. maven-invoker-plugin ${invoker-plugin.version} + + org.codehaus.mojo + license-maven-plugin + ${license-plugin.version} + org.codehaus.mojo tidy-maven-plugin @@ -1036,6 +1053,15 @@ limitations under the License. org.apache.maven.plugins maven-failsafe-plugin + + + default + + integration-test + verify + + + @@ -1299,5 +1325,18 @@ limitations under the License. examples + + + cli + + + + !learnlib.release + + + + cli + + diff --git a/test-support/test-support/pom.xml b/test-support/test-support/pom.xml index a93d6e987..b2d3292ea 100644 --- a/test-support/test-support/pom.xml +++ b/test-support/test-support/pom.xml @@ -58,8 +58,8 @@ - org.apache.fury - fury-core + org.apache.fory + fory-core org.mockito diff --git a/test-support/test-support/src/main/java/de/learnlib/testsupport/ResumeUtils.java b/test-support/test-support/src/main/java/de/learnlib/testsupport/ResumeUtils.java index 05445ca9a..162525602 100644 --- a/test-support/test-support/src/main/java/de/learnlib/testsupport/ResumeUtils.java +++ b/test-support/test-support/src/main/java/de/learnlib/testsupport/ResumeUtils.java @@ -16,19 +16,32 @@ package de.learnlib.testsupport; import de.learnlib.Resumable; -import org.apache.fury.Fury; -import org.apache.fury.logging.LoggerFactory; +import org.apache.fory.Fory; +import org.apache.fory.logging.LoggerFactory; +import org.apache.fory.resolver.AllowListChecker; +import org.apache.fory.resolver.AllowListChecker.CheckLevel; /** * Utility functions for {@link Resumable} features. */ public final class ResumeUtils { - private static final Fury FURY; + private static final Fory FORY; static { + // use same config as CLI to automatically test proper white-listing + final AllowListChecker checker = new AllowListChecker(); + checker.setCheckLevel(CheckLevel.STRICT); + checker.allowClass("de.learnlib.*"); + checker.allowClass("net.automatalib.*"); + FORY = Fory.builder() + .requireClassRegistration(false) + .withCodegen(false) + .withRefTracking(true) + .withTypeChecker(checker) + .withXlang(false) + .build(); LoggerFactory.useSlf4jLogging(true); - FURY = Fury.builder().withRefTracking(true).requireClassRegistration(false).build(); } private ResumeUtils() { @@ -36,12 +49,12 @@ private ResumeUtils() { } public static byte[] toBytes(Object state) { - return FURY.serialize(state); + return FORY.serialize(state); } @SuppressWarnings("unchecked") public static T fromBytes(byte[] bytes) { - return (T) FURY.deserialize(bytes); + return (T) FORY.deserialize(bytes); } } diff --git a/test-support/test-support/src/main/java/module-info.java b/test-support/test-support/src/main/java/module-info.java index ee6b8d3ca..363ec637b 100644 --- a/test-support/test-support/src/main/java/module-info.java +++ b/test-support/test-support/src/main/java/module-info.java @@ -39,7 +39,7 @@ requires net.automatalib.common.util; requires net.automatalib.core; requires net.automatalib.util; - requires org.apache.fury.core; + requires org.apache.fory.core; requires org.mockito; requires org.testng;