From 0a705a828239e3f6619372c0db191f28b1e4f53e Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Tue, 21 Jul 2026 01:24:21 +0200 Subject: [PATCH 01/34] initial work on CLI application --- cli/pom.xml | 212 +++++++++++++++ .../java/de/learnlib/cli/Application.java | 71 +++++ .../cli/adapter/ProceduralDFAAdapter.java | 75 ++++++ .../cli/adapter/ProceduralMealyAdapter.java | 94 +++++++ .../learnlib/cli/factory/AlphabetFactory.java | 70 +++++ .../de/learnlib/cli/factory/EQOFactory.java | 235 +++++++++++++++++ .../learnlib/cli/factory/LearnerFactory.java | 139 ++++++++++ .../de/learnlib/cli/factory/MQOFactory.java | 129 +++++++++ .../cli/factory/SerializerFactory.java | 141 ++++++++++ .../de/learnlib/cli/option/EQOParams.java | 246 ++++++++++++++++++ .../java/de/learnlib/cli/option/EQOracle.java | 29 +++ .../java/de/learnlib/cli/option/Learner.java | 29 +++ .../java/de/learnlib/cli/option/Options.java | 71 +++++ .../java/de/learnlib/cli/option/Output.java | 27 ++ .../java/de/learnlib/cli/option/Symbols.java | 59 +++++ .../java/de/learnlib/cli/option/Type.java | 99 +++++++ .../de/learnlib/cli/util/Constructor.java | 46 ++++ .../java/de/learnlib/cli/util/Runner.java | 106 ++++++++ .../de/learnlib/cli/util/VersionProvider.java | 29 +++ cli/src/main/java/module-info.java | 52 ++++ cli/src/main/resources/application.properties | 69 +++++ cli/src/main/resources/logback.xml | 28 ++ .../java/de/learnlib/cli/ApplicationIT.java | 156 +++++++++++ cli/src/test/resources/dfa.dot | 13 + cli/src/test/resources/mealy.dot | 10 + cli/src/test/resources/suls/stateful.py | 28 ++ cli/src/test/resources/suls/stateful_stdin.py | 28 ++ cli/src/test/resources/suls/stateless.py | 14 + .../test/resources/suls/stateless_stdin.py | 15 ++ distribution/src/it/jlink/pom.xml | 5 - .../oracle/equivalence/EQOracleChain.java | 5 + .../learnlib/oracle/membership/CLIOracle.java | 28 +- .../oracle/membership/CLIOutputOracle.java | 25 +- .../oracle/membership/StdInOracle.java | 33 ++- .../oracle/membership/StdInOutputOracle.java | 28 +- pom.xml | 42 ++- 36 files changed, 2458 insertions(+), 28 deletions(-) create mode 100644 cli/pom.xml create mode 100644 cli/src/main/java/de/learnlib/cli/Application.java create mode 100644 cli/src/main/java/de/learnlib/cli/adapter/ProceduralDFAAdapter.java create mode 100644 cli/src/main/java/de/learnlib/cli/adapter/ProceduralMealyAdapter.java create mode 100644 cli/src/main/java/de/learnlib/cli/factory/AlphabetFactory.java create mode 100644 cli/src/main/java/de/learnlib/cli/factory/EQOFactory.java create mode 100644 cli/src/main/java/de/learnlib/cli/factory/LearnerFactory.java create mode 100644 cli/src/main/java/de/learnlib/cli/factory/MQOFactory.java create mode 100644 cli/src/main/java/de/learnlib/cli/factory/SerializerFactory.java create mode 100644 cli/src/main/java/de/learnlib/cli/option/EQOParams.java create mode 100644 cli/src/main/java/de/learnlib/cli/option/EQOracle.java create mode 100644 cli/src/main/java/de/learnlib/cli/option/Learner.java create mode 100644 cli/src/main/java/de/learnlib/cli/option/Options.java create mode 100644 cli/src/main/java/de/learnlib/cli/option/Output.java create mode 100644 cli/src/main/java/de/learnlib/cli/option/Symbols.java create mode 100644 cli/src/main/java/de/learnlib/cli/option/Type.java create mode 100644 cli/src/main/java/de/learnlib/cli/util/Constructor.java create mode 100644 cli/src/main/java/de/learnlib/cli/util/Runner.java create mode 100644 cli/src/main/java/de/learnlib/cli/util/VersionProvider.java create mode 100644 cli/src/main/java/module-info.java create mode 100644 cli/src/main/resources/application.properties create mode 100644 cli/src/main/resources/logback.xml create mode 100644 cli/src/test/java/de/learnlib/cli/ApplicationIT.java create mode 100644 cli/src/test/resources/dfa.dot create mode 100644 cli/src/test/resources/mealy.dot create mode 100755 cli/src/test/resources/suls/stateful.py create mode 100755 cli/src/test/resources/suls/stateful_stdin.py create mode 100755 cli/src/test/resources/suls/stateless.py create mode 100755 cli/src/test/resources/suls/stateless_stdin.py diff --git a/cli/pom.xml b/cli/pom.xml new file mode 100644 index 000000000..3a2c57d6e --- /dev/null +++ b/cli/pom.xml @@ -0,0 +1,212 @@ + + + + 4.0.0 + + + de.learnlib + learnlib-build-parent + 0.19.0-SNAPSHOT + ../build-parent/pom.xml + + + learnlib-cli + + LearnLib :: CLI + + A standalone application to offer the functionality of LearnLib via a command-line interface. + *Note:* This artifact is not intended as a library and therefore not deployed to Maven Central but instead as a + direct download (e.g., from GitHub). + + + + + + + de.learnlib + learnlib-api + + + de.learnlib + learnlib-cache + + + de.learnlib + learnlib-membership-oracles + + + de.learnlib + learnlib-util + + + de.learnlib + learnlib-statistics + + + + + de.learnlib + learnlib-dhc + + + de.learnlib + learnlib-equivalence-oracles + + + de.learnlib + learnlib-kearns-vazirani + + + de.learnlib + learnlib-lambda + + + 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-util + + + + ch.qos.logback + logback-classic + + + info.picocli + picocli + + + org.slf4j + slf4j-api + + + + + org.testng + testng + + + + + + + + + src/main/resources + true + + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + org.apache.maven.plugins + maven-jlink-plugin + + + package + + jlink + + + false + true + true + true + learnlib-cli-${project.version} + learnlib=de.learnlib.cli/de.learnlib.cli.Application + + + + + + + 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..8cd9e640f --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/Application.java @@ -0,0 +1,71 @@ +/* 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 = "Stores the current contents of the index in a new commit along with a log message from the user describing the changes.") +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().run(options); + } + + private void setLogLevel(Options options) { + if (options.verbosity != null) { + final Logger root = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); + + final Level level = switch (options.verbosity.length) { + case 0 -> Level.INFO; + case 1 -> Level.DEBUG; + default -> 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..0823ca1b5 --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/adapter/ProceduralDFAAdapter.java @@ -0,0 +1,75 @@ +/* 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.observationpack.dfa.OPLearnerDFA; +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 OPLearnerDFAAdapter extends OPLearnerDFA implements ProceduralDFAAdapter { + + public OPLearnerDFAAdapter(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..2b9c91464 --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/adapter/ProceduralMealyAdapter.java @@ -0,0 +1,94 @@ +/* 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.observationpack.mealy.OPLearnerMealy; +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 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 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..0ca9199c6 --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/factory/AlphabetFactory.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.factory; + +import java.util.Objects; +import java.util.function.Function; + +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; + +@FunctionalInterface +public interface AlphabetFactory> extends Function { + + AlphabetFactory> REGULAR = AlphabetFactory::getRegularAlphabet; + AlphabetFactory> PROCEDURAL = AlphabetFactory::getProceduralAlphabet; + AlphabetFactory> VPA = AlphabetFactory::getVPAAlphabet; + + private static Alphabet getRegularAlphabet(Options options) { + final RegularSymbols inputs = validateRegularSymbols(options); + return Alphabets.fromList(inputs.symbols); + } + + private 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)); + } + + private static VPAlphabet getVPAAlphabet(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..e836362dd --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/factory/EQOFactory.java @@ -0,0 +1,235 @@ +/* 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.function.BiFunction; + +import de.learnlib.cli.option.EQOracle; +import de.learnlib.cli.option.Options; +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 net.automatalib.alphabet.impl.Alphabets; +import net.automatalib.automaton.UniversalDeterministicAutomaton; +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.transducer.MealyMachine; +import net.automatalib.automaton.vpa.OneSEVPA; +import net.automatalib.util.automaton.fsa.NFAs; +import net.automatalib.word.Word; + +@FunctionalInterface +public interface EQOFactory extends BiFunction, EquivalenceOracle> { + + EQOFactory, String, Boolean> DFA_ORACLES = EQOFactory::getRegularOracles; + EQOFactory, String, Word> MEALY_ORACLES = EQOFactory::getRegularOracles; + EQOFactory, String, Boolean> NFA_ORACLES = EQOFactory::getNFAOracles; + EQOFactory, String, Boolean> SBA_ORACLES = EQOFactory::getSBAOracles; + EQOFactory, String, Boolean> SPA_ORACLES = EQOFactory::getSPAOracles; + EQOFactory, String, Word> SPMM_ORACLES = EQOFactory::getSPMMOracles; + EQOFactory, String, Boolean> VPA_ORACLES = EQOFactory::getVPAOracles; + + private 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); + case WP -> new WpMethodEQOracle<>(mqo, + options.eqoParams.wpMethod.lookahead, + options.eqoParams.wpMethod.expectedSize); + case RANDOM -> new RandomWordsEQOracle<>(mqo, + options.eqoParams.random.minLength, + options.eqoParams.random.maxLength, + options.eqoParams.random.maxTests, + new Random(options.eqoParams.random.seed)); + case RANDOM_W -> new RandomWMethodEQOracle<>(mqo, + options.eqoParams.randomWMethod.minimalSize, + options.eqoParams.randomWMethod.rndLength, + options.eqoParams.randomWMethod.bound, + new Random(options.eqoParams.randomWMethod.seed), + 1); + case RANDOM_WP -> new RandomWpMethodEQOracle<>(mqo, + options.eqoParams.randomWpMethod.minimalSize, + options.eqoParams.randomWpMethod.rndLength, + options.eqoParams.randomWpMethod.bound, + new Random(options.eqoParams.randomWpMethod.seed), + 1); + case KWAY_S -> new KWayStateCoverEQOracle<>(mqo, + new Random(options.eqoParams.kWayState.seed), + options.eqoParams.kWayState.randomWalkLen, + options.eqoParams.kWayState.k, + options.eqoParams.kWayState.combinationMethod, + 1); + 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, + 1); + case SAMPLE -> buildSampleSetOracle(options, mqo); + }; + chain.addOracle(eqo); + } + + return chain; + } + + private static EquivalenceOracle, 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; + } + + private static EquivalenceOracle, 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); + case RANDOM -> buildRandomWellMatchedOracle(options, mqo); + case SAMPLE -> buildSampleSetOracle(options, mqo); + default -> throw new UnsupportedCombinationException(options, e); + }; + chain.addOracle(eqo); + } + + return chain; + } + + private static EquivalenceOracle, 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); + case WP -> new de.learnlib.oracle.equivalence.spa.WpMethodEQOracle<>(mqo, + options.eqoParams.wpMethod.lookahead, + options.eqoParams.wpMethod.expectedSize); + case RANDOM -> buildRandomWellMatchedOracle(options, mqo); + case SAMPLE -> buildSampleSetOracle(options, mqo); + default -> throw new UnsupportedCombinationException(options, e); + }; + chain.addOracle(eqo); + } + + return chain; + } + + private static EquivalenceOracle, 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); + case SAMPLE -> buildSampleSetOracle(options, mqo); + default -> throw new UnsupportedCombinationException(options, e); + }; + chain.addOracle(eqo); + } + + return chain; + } + + private static EquivalenceOracle, 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) { + final double callProb = 0.5; + return new RandomWellMatchedWordsEQOracle<>(new Random(options.eqoParams.random.seed), + oracle, + callProb, + options.eqoParams.random.maxTests, + options.eqoParams.random.minLength, + options.eqoParams.random.maxLength); + } + + private static SampleSetEQOracle buildSampleSetOracle(Options options, + MembershipOracle oracle) { + final List samples = options.eqoParams.samples.samples; + final List> tmp = new ArrayList<>(samples.size()); + + for (String s : options.eqoParams.samples.samples) { + String[] words = s.split(options.eqoParams.samples.split); + tmp.add(Word.fromArray(words, 0, words.length)); + } + + return new SampleSetEQOracle().addAll(oracle, tmp); + } + + 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..a9dc89357 --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/factory/LearnerFactory.java @@ -0,0 +1,139 @@ +/* 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.function.Function; + +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.OPLearnerDFAAdapter; +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.MealyDHCAdapter; +import de.learnlib.cli.adapter.ProceduralMealyAdapter.OPLearnerMealyAdapter; +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; +import de.learnlib.cli.util.Constructor.DFAConstructor; +import de.learnlib.cli.util.Constructor.MealyConstructor; +import net.automatalib.alphabet.Alphabet; +import net.automatalib.alphabet.ProceduralInputAlphabet; +import net.automatalib.alphabet.VPAlphabet; +import net.automatalib.automaton.concept.FiniteRepresentation; +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.word.Word; + +@FunctionalInterface +public interface LearnerFactory, M extends FiniteRepresentation, D> + extends Function> { + + LearnerFactory, DFA, Boolean> DFA_LEARNER = LearnerFactory::getDFALearner; + LearnerFactory, MealyMachine, Word> MEALY_LEARNER = + LearnerFactory::getMealyLearner; + LearnerFactory, NFA, Boolean> NFA_LEARNER = LearnerFactory::getNFALearner; + LearnerFactory, SBA, Boolean> SBA_LEARNER = + LearnerFactory::getSBALearner; + LearnerFactory, SPA, Boolean> SPA_LEARNER = + LearnerFactory::getSPALearner; + LearnerFactory, SPMM, Word> SPMM_LEARNER = + LearnerFactory::getSPMMLearner; + LearnerFactory, OneSEVPA, Boolean> VPA_LEARNER = LearnerFactory::getVPALearner; + + private static DFAConstructor, I> getDFALearner(Options options) { + return switch (options.learner) { + case KV -> KearnsVaziraniDFAAdapter::new; + case LLAMBDA -> LLambdaDFAAdapter::new; + case LSTAR -> ExtensibleLStarDFAAdapter::new; + case OP -> OPLearnerDFAAdapter::new; + case TTT -> TTTLearnerDFAAdapter::new; + case TTTLAMBDA -> TTTLambdaDFAAdapter::new; + default -> throw new UnsupportedCombinationException(options); + }; + } + + private static MealyConstructor, I, O> getMealyLearner(Options options) { + return switch (options.learner) { + case DHC -> MealyDHCAdapter::new; + case KV -> KearnsVaziraniMealyAdapter::new; + case LLAMBDA -> LLambdaMealyAdapter::new; + case LSTAR -> ExtensibleLStarMealyAdapter::new; + case SPARSE -> SparseLearnerAdapter::new; + case OP -> OPLearnerMealyAdapter::new; + case TTT -> TTTLearnerMealyAdapter::new; + case TTTLAMBDA -> TTTLambdaMealyAdapter::new; + default -> throw new UnsupportedCombinationException(options); + }; + } + + private static Constructor, NFA, I, Boolean> getNFALearner(Options options) { + if (options.learner == Learner.NLSTAR) { + return NLStarLearner::new; + } + throw new UnsupportedCombinationException(options); + } + + private static Constructor, SBA, I, Boolean> getSBALearner(Options options) { + final DFAConstructor>, SymbolWrapper> learner = getDFALearner(options); + return (alph, mqo) -> new SBALearner<>(alph, mqo, learner::constructLearner); + } + + private static Constructor, SPA, I, Boolean> getSPALearner(Options options) { + final DFAConstructor, I> learner = getDFALearner(options); + return (alph, mqo) -> new SPALearner<>(alph, mqo, learner::constructLearner); + } + + private static Constructor, SPMM, I, Word> getSPMMLearner( + Options options) { + final MealyConstructor>, SymbolWrapper, String> learner = getMealyLearner(options); + return (alph, mqo) -> new SPMMLearner<>(alph, "error", mqo, learner::constructLearner); + } + + private static Constructor, OneSEVPA, I, Boolean> getVPALearner(Options options) { + return switch (options.learner) { + case OP -> (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); + }; + } + + 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..fe18c9374 --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/factory/MQOFactory.java @@ -0,0 +1,129 @@ +/* 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.Collections; +import java.util.List; +import java.util.function.BiFunction; +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.CounterOracle; +import de.learnlib.oracle.MembershipOracle; +import de.learnlib.oracle.membership.CLIOracle; +import de.learnlib.oracle.membership.CLIOutputOracle; +import de.learnlib.oracle.membership.StdInOracle; +import de.learnlib.oracle.membership.StdInOutputOracle; +import net.automatalib.word.Word; + +@FunctionalInterface +public interface MQOFactory extends Function> { + + MQOFactory ACCEPTOR = MQOFactory::getAcceptorOracle; + MQOFactory> TRANSDUCER = MQOFactory::getTransducerOracle; + + private static MembershipOracle getAcceptorOracle(Options options) { + verifyPath(options); + + MembershipOracle oracle; + + if (options.stdin) { + oracle = new StdInOracle<>(buildCommandLine(options), options.reset); + } else { + oracle = new CLIOracle<>(buildCommandLine(options), options.reset); + + } + + if (options.statistics) { + oracle = new CounterOracle<>(oracle, "sul"); + } + + if (options.cache) { + oracle = DFACaches.createHashCache(oracle); + if (options.statistics) { + oracle = new CounterOracle<>(oracle, "cache"); + } + } + + return oracle; + } + + private static MembershipOracle> getTransducerOracle(Options options) { + verifyPath(options); + + MembershipOracle> oracle; + + if (options.stdin) { + oracle = new StdInOutputOracle<>(buildCommandLine(options), new OutputTransformer(options), options.reset); + } else { + oracle = new CLIOutputOracle<>(buildCommandLine(options), new OutputTransformer(options), options.reset); + + } + + if (options.statistics) { + oracle = new CounterOracle<>(oracle, "sul"); + } + + if (options.cache) { + oracle = MealyCaches.createDynamicTreeCache(oracle); + if (options.statistics) { + oracle = new CounterOracle<>(oracle, "cache"); + } + } + + return oracle; + } + + private static List buildCommandLine(Options options) { + final String absolutePath = options.sul.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(Options options) { + if (!options.sul.exists()) { + throw new IllegalArgumentException(String.format("Specified SUL '%s' does not exist", options.sul)); + } + } + + class OutputTransformer implements BiFunction> { + + private final Pattern pattern; + + public OutputTransformer(Options options) { + this.pattern = Pattern.compile(options.delimiter); + } + + @Override + public Word apply(String string, Integer offset) { + if (string.isEmpty()) { + return Word.epsilon(); + } + final String[] orig = pattern.split(string); + return Word.fromArray(orig, offset, orig.length - offset); + } + } +} 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..039fd8462 --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/factory/SerializerFactory.java @@ -0,0 +1,141 @@ +/* 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 java.util.function.Function; + +import de.learnlib.cli.option.Options; +import de.learnlib.cli.option.Output; +import net.automatalib.automaton.Automaton; +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.graph.concept.GraphViewable; +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.ts.simple.SimpleTS; + +@FunctionalInterface +public interface SerializerFactory> + extends Function> { + + SerializerFactory> DFA_SERIALIZER = SerializerFactory::getDFASerializer; + SerializerFactory> MEALY_SERIALIZER = SerializerFactory::getMealySerializer; + SerializerFactory> NFA_SERIALIZER = SerializerFactory::getNFASerializer; + SerializerFactory> SBA_SERIALIZER = SerializerFactory::getSBASerializer; + SerializerFactory> SPA_SERIALIZER = SerializerFactory::getSPASerializer; + SerializerFactory> SPMM_SERIALIZER = SerializerFactory::getSPMMSerializer; + SerializerFactory> VPA_SERIALIZER = SerializerFactory::getVPASerializer; + + private 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 -> dotAutomatonWriter(); + case LEARNLIBV2 -> LearnLibV2Serialization.getInstance(); + case MATA -> new MataNFAWriter<>(); + case SAF -> SAFWriters.dfa(); + // case TAF -> TAFWriters.dfa(); + default -> throw new UnsupportedCombinationException(options); + }; + } + + private static InputModelSerializer> getMealySerializer(Options options) { + return switch (options.format) { + case DOT -> dotAutomatonWriter(); + case SAF -> SAFWriters.mealy(DataOutput::writeUTF); + // case TAF -> TAFWriters.mealy(); + default -> throw new UnsupportedCombinationException(options); + }; + } + + private 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 -> dotAutomatonWriter(); + case MATA -> new MataNFAWriter<>(); + case SAF -> SAFWriters.nfa(); + default -> throw new UnsupportedCombinationException(options); + }; + } + + private static InputModelSerializer> getSBASerializer(Options options) { + if (options.format == Output.DOT) { + return dotGraphWriter(); + } + throw new UnsupportedCombinationException(options); + } + + private static InputModelSerializer> getSPASerializer(Options options) { + if (options.format == Output.DOT) { + return dotGraphWriter(); + } + throw new UnsupportedCombinationException(options); + } + + private static InputModelSerializer> getSPMMSerializer(Options options) { + if (options.format == Output.DOT) { + return dotGraphWriter(); + } + throw new UnsupportedCombinationException(options); + } + + private static InputModelSerializer> getVPASerializer(Options options) { + if (options.format == Output.DOT) { + return dotGraphWriter(); + } + throw new UnsupportedCombinationException(options); + } + + private static > InputModelSerializer dotAutomatonWriter() { + return (os, model, inputs) -> DOTSerializationProvider.getInstance() + .writeModel(os, model.transitionGraphView(inputs)); + } + + private static & GraphViewable> InputModelSerializer dotGraphWriter() { + return (os, model, inputs) -> DOTSerializationProvider.getInstance().writeModel(os, model.graphView()); + } + + 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..d65f7542c --- /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-len", + defaultValue = "20", + paramLabel = "", + descriptionKey = "param.eqo.kway-s.len") + public int randomWalkLen; + + @Option(names = "--eqo-kway-s-method", + defaultValue = "PERMUTATIONS", + paramLabel = "", + descriptionKey = "param.eqo.kway-s.method") + 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-len", + defaultValue = "10", + paramLabel = "", + descriptionKey = "param.eqo.kway-t.len") + public int randomWalkLen; + + @Option(names = "--eqo-kway-t-num", + defaultValue = "1000", + paramLabel = "", + descriptionKey = "param.eqo.kway-t.num") + public int numGeneratePaths; + + @Option(names = "--eqo-kway-t-path", + defaultValue = "50", + paramLabel = "", + descriptionKey = "param.eqo.kway-t.path") + public int maxPathLen; + + @Option(names = "--eqo-kway-t-step", + defaultValue = "100", + paramLabel = "", + descriptionKey = "param.eqo.kway-t.step") + public int maxNumberOfSteps; + + @Option(names = "--eqo-kway-t-metric", + defaultValue = "STEPS", + paramLabel = "", + descriptionKey = "param.eqo.kway-t.metric") + public OptimizationMetric optimizationMetric; + + @Option(names = "--eqo-kway-t-method", + defaultValue = "RANDOM", + paramLabel = "", + descriptionKey = "param.eqo.kway-t.method") + 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-min", + defaultValue = "10", + paramLabel = "", + descriptionKey = "param.eqo.random.min") + public int minLength; + + @Option(names = "--eqo-random-max", + defaultValue = "20", + paramLabel = "", + descriptionKey = "param.eqo.random.max") + public int maxLength; + + @Option(names = "--eqo-random-num", + defaultValue = "1000", + paramLabel = "", + descriptionKey = "param.eqo.random.num") + 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-min", + defaultValue = "0", + paramLabel = "", + descriptionKey = "param.eqo.random-w.min") + public int minimalSize; + + @Option(names = "--eqo-random-w-len", + defaultValue = "5", + paramLabel = "", + descriptionKey = "param.eqo.random-w.len") + public int rndLength; + + @Option(names = "--eqo-random-w-bound", + defaultValue = "1000", + 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-min", + defaultValue = "0", + paramLabel = "", + descriptionKey = "param.eqo.random-wp.min") + public int minimalSize; + + @Option(names = "--eqo-random-wp-len", + defaultValue = "5", + paramLabel = "", + descriptionKey = "param.eqo.random-wp.len") + public int rndLength; + + @Option(names = "--eqo-random-wp-bound", + defaultValue = "1000", + 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 = " ", + 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-expected", + defaultValue = "5", + paramLabel = "", + descriptionKey = "param.eqo.w.expected") + 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-expected", + defaultValue = "5", + paramLabel = "", + descriptionKey = "param.eqo.wp.expected") + 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..36744a865 --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/option/Learner.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 Learner { + + DHC, + KV, + LLAMBDA, + LSTAR, + NLSTAR, + OP, + SPARSE, + TTT, + TTTLAMBDA, +} 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..abe994a1f --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/option/Options.java @@ -0,0 +1,71 @@ +/* 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.util.List; + +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +public class Options { + + @Parameters(paramLabel = "", descriptionKey = "option.sul") + public File 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 File output; + + @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..910736090 --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/option/Type.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.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.Runner; + +public enum Type { + DFA { + @Override + public Runner runner() { + return new Runner<>(AlphabetFactory.REGULAR, + MQOFactory.ACCEPTOR, + LearnerFactory.DFA_LEARNER, + EQOFactory.DFA_ORACLES, + SerializerFactory.DFA_SERIALIZER); + } + }, + MEALY { + @Override + public Runner runner() { + return new Runner<>(AlphabetFactory.REGULAR, + MQOFactory.TRANSDUCER, + LearnerFactory.MEALY_LEARNER, + EQOFactory.MEALY_ORACLES, + SerializerFactory.MEALY_SERIALIZER); + } + }, + NFA { + @Override + public Runner runner() { + return new Runner<>(AlphabetFactory.REGULAR, + MQOFactory.ACCEPTOR, + LearnerFactory.NFA_LEARNER, + EQOFactory.NFA_ORACLES, + SerializerFactory.NFA_SERIALIZER); + } + }, + SBA { + @Override + public Runner runner() { + return new Runner<>(AlphabetFactory.PROCEDURAL, + MQOFactory.ACCEPTOR, + LearnerFactory.SBA_LEARNER, + EQOFactory.SBA_ORACLES, + SerializerFactory.SBA_SERIALIZER); + } + }, + SPA { + @Override + public Runner runner() { + return new Runner<>(AlphabetFactory.PROCEDURAL, + MQOFactory.ACCEPTOR, + LearnerFactory.SPA_LEARNER, + EQOFactory.SPA_ORACLES, + SerializerFactory.SPA_SERIALIZER); + } + }, + SPMM { + @Override + public Runner runner() { + return new Runner<>(AlphabetFactory.PROCEDURAL, + MQOFactory.TRANSDUCER, + LearnerFactory.SPMM_LEARNER, + EQOFactory.SPMM_ORACLES, + SerializerFactory.SPMM_SERIALIZER); + } + }, + VPA { + @Override + public Runner runner() { + return new Runner<>(AlphabetFactory.VPA, + MQOFactory.ACCEPTOR, + LearnerFactory.VPA_LEARNER, + EQOFactory.VPA_ORACLES, + SerializerFactory.VPA_SERIALIZER); + } + }; + + public abstract Runner runner(); + +} 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..d9e8fd71a --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/util/Constructor.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 de.learnlib.algorithm.LearningAlgorithm; +import de.learnlib.cli.adapter.ProceduralDFAAdapter; +import de.learnlib.cli.adapter.ProceduralMealyAdapter; +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> { + + LearningAlgorithm constructLearner(A alphabet, MembershipOracle oracle); + + @FunctionalInterface + interface MealyConstructor, I, O> + extends Constructor, I, Word> { + + @Override + ProceduralMealyAdapter constructLearner(A alphabet, MembershipOracle> oracle); + } + + @FunctionalInterface + interface DFAConstructor, I> extends Constructor, I, Boolean> { + + @Override + ProceduralDFAAdapter constructLearner(A alphabet, MembershipOracle oracle); + } +} 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..bc6f82cf8 --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/util/Runner.java @@ -0,0 +1,106 @@ +/* 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.util.function.BiFunction; +import java.util.function.Function; + +import de.learnlib.algorithm.LearningAlgorithm; +import de.learnlib.cli.option.Options; +import de.learnlib.filter.statistic.oracle.CounterOracle; +import de.learnlib.oracle.EquivalenceOracle; +import de.learnlib.oracle.MembershipOracle; +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 class Runner, M extends SimpleTS & FiniteRepresentation, I, D> { + + private static final Logger LOGGER = LoggerFactory.getLogger(Runner.class); + + private final Function alphabetCreator; + private final Function> mqoCreator; + private final Function> learnerCreator; + private final BiFunction, EquivalenceOracle> eqoCreator; + private final Function> serializerCreator; + + public Runner(Function alphabetCreator, + Function> mqoCreator, + Function> learnerCreator, + BiFunction, EquivalenceOracle> eqoCreator, + Function> serializerCreator) { + this.alphabetCreator = alphabetCreator; + this.mqoCreator = mqoCreator; + this.learnerCreator = learnerCreator; + this.eqoCreator = eqoCreator; + this.serializerCreator = serializerCreator; + } + + @SuppressWarnings("PMD.AvoidThrowingRawExceptionTypes") + public void run(Options options) { + + final A alphabet = alphabetCreator.apply(options); + final MembershipOracle mqo = mqoCreator.apply(options); + + final MembershipOracle learnerOracle; + if (options.statistics) { + learnerOracle = new CounterOracle<>(mqo, "learner"); + } else { + learnerOracle = mqo; + } + + final LearningAlgorithm learner = + learnerCreator.apply(options).constructLearner(alphabet, learnerOracle); + + final MembershipOracle eqoOracle; + if (options.statistics) { + eqoOracle = new CounterOracle<>(mqo, "eqo"); + } else { + eqoOracle = mqo; + } + + final EquivalenceOracle eqo = eqoCreator.apply(options, eqoOracle); + final InputModelSerializer serializer = serializerCreator.apply(options); + + final Experiment experiment = new Experiment<>(learner, eqo, alphabet); + experiment.run(); + + final M hyp = experiment.getFinalHypothesis(); + + if (options.statistics) { + StatisticsService service = Statistics.getService(); + LOGGER.info(service.print()); + service.clear(); + } + + try { + if (options.output != null) { + serializer.writeModel(options.output, hyp, alphabet); + } else { + serializer.writeModel(System.out, hyp, alphabet); + } + } catch (IOException e) { + throw new RuntimeException(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..c751ee9ad --- /dev/null +++ b/cli/src/main/java/module-info.java @@ -0,0 +1,52 @@ +/* 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.dhc; + requires de.learnlib.algorithm.kv; + requires de.learnlib.algorithm.lambda; + 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 net.automatalib.api; + requires net.automatalib.common.util; + requires net.automatalib.core; + requires net.automatalib.serialization.dot; + requires net.automatalib.serialization.saf; + requires net.automatalib.serialization.mata; + requires net.automatalib.serialization.aut; + requires net.automatalib.serialization.ba; + requires net.automatalib.serialization.learnlibv2; + requires net.automatalib.util; + + requires ch.qos.logback.classic; + requires info.picocli; + requires org.slf4j; + +} diff --git a/cli/src/main/resources/application.properties b/cli/src/main/resources/application.properties new file mode 100644 index 000000000..48b3891c4 --- /dev/null +++ b/cli/src/main/resources/application.properties @@ -0,0 +1,69 @@ +# 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. +option.cache=Use a query cache. +option.delim=The string used to split responses of the SUL into individual output symbols. +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. If omitted, the final hypothesis model will be printed to standard out. +option.reset=Specifies the reset symbol. If provided, the SUL 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.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 binary. +option.sul=The path to the SUL used for answering queries. +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.k=The 'k' parameter of the kWay state cover oracle. +param.eqo.kway-s.len=The 'randomWalkLen' parameter of the kWay state cover oracle. +param.eqo.kway-s.method=The 'combinationMethod' parameter of the kWay state cover oracle. Possible values are: ${COMPLETION-CANDIDATES}. +param.eqo.kway-s.seed=The seed of the 'random' parameter of the kWay state cover oracle. +param.eqo.kway-t.k=The 'k' parameter of the kWay transition cover oracle. +param.eqo.kway-t.len=The 'randomWalkLen' parameter of the kWay transition cover oracle. +param.eqo.kway-t.method=The 'generationMethod' parameter of the kWay transition cover oracle. Possible values are: ${COMPLETION-CANDIDATES}. +param.eqo.kway-t.metric=The 'optimizationMetric' parameter of the kWay transition cover oracle. Possible values are: ${COMPLETION-CANDIDATES}. +param.eqo.kway-t.num=The 'numGeneratePaths' parameter of the kWay transition cover oracle. +param.eqo.kway-t.path=The 'maxPathLen' parameter of the kWay transition cover oracle. +param.eqo.kway-t.seed=The seed of the 'random' parameter of the kWay transition cover oracle. +param.eqo.kway-t.step=The 'maxNumberOfSteps' parameter of the kWay transition cover oracle. +param.eqo.random-w.bound=The 'bound' parameter of the random W method oracle. +param.eqo.random-w.len=The 'rndLength' parameter of the random W method oracle. +param.eqo.random-w.min=The 'minimalSize' parameter of the random W method oracle. +param.eqo.random-w.seed=The seed of the 'random' parameter of the random W method oracle. +param.eqo.random-wp.bound=The 'bound' parameter of the random Wp method oracle. +param.eqo.random-wp.len=The 'rndLength' parameter of the random Wp method oracle. +param.eqo.random-wp.min=The 'minimalSize' parameter of the random Wp method oracle. +param.eqo.random-wp.seed=The seed of the 'random' parameter of the random Wp method oracle. +param.eqo.random.max=The 'maxLength' parameter of the random word oracle. +param.eqo.random.min=The 'minLength' parameter of the random word oracle. +param.eqo.random.num=The 'maxTests' parameter of the random word oracle. +param.eqo.random.seed=The seed of the 'random' parameter of the random W method oracle. +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 to split and individual symbols of a trace. +param.eqo.w.expected=The 'expectedSize' parameter for the W method oracle. +param.eqo.w.lookahead=The 'lookahead' parameter for the W method oracle. +param.eqo.wp.expected=The 'expectedSize' parameter for the Wp method oracle. +param.eqo.wp.lookahead=The 'lookahead' parameter for the Wp method oracle. + +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..2aceeb7fc --- /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 + + + + + + + \ No newline at end of file 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..2d57768f5 --- /dev/null +++ b/cli/src/test/java/de/learnlib/cli/ApplicationIT.java @@ -0,0 +1,156 @@ +/* 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 net.automatalib.common.util.IOUtil; +import net.automatalib.common.util.process.ProcessUtil; +import org.testng.Assert; +import org.testng.SkipException; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; +import picocli.CommandLine; + +public class ApplicationIT { + + private static final String STATELESS = getPathToScript("/suls/stateless.py"); + private static final String STATELESS_STDIN = getPathToScript("/suls/stateless_stdin.py"); + private static final String STATEFUL = getPathToScript("/suls/stateful.py"); + private static final String STATEFUL_STDIN = getPathToScript("/suls/stateful_stdin.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); + } + } + + @BeforeTest + public void setUp() { + try { + if (ProcessUtil.invokeProcess(new String[] {"python", "--version"}) != 0) { + throw new SkipException("python not supported"); + } + } catch (IOException | InterruptedException e) { + throw new SkipException("python not supported"); + } + } + + @Test + public void testStatelessMealy() throws IOException { + checkExecution(new String[] {"-sa", "-sb", "-tMEALY", "--eqo-random-wp-bound", "100", STATELESS}, "/mealy.dot"); + } + + @Test + public void testStatelessDFA() throws IOException { + checkExecution(new String[] {"-sa", "-sb", "--eqo-random-wp-bound", "100", STATELESS}, "/dfa.dot"); + } + + @Test + public void testStatelessStdInMealy() throws IOException { + checkExecution(new String[] {"-sa", "-sb", "-tMEALY", "--stdin", "-eWP", STATELESS_STDIN}, "/mealy.dot"); + } + + @Test + public void testStatelessStdInDFA() throws IOException { + checkExecution(new String[] {"-sa", "-sb", "--stdin", "-eWP", STATELESS_STDIN}, "/dfa.dot"); + } + + @Test + public void testStatefulMealy() throws IOException { + checkExecution(new String[] {"-sa", + "-sb", + "-tMEALY", + "--reset", + "reset", + "-eSAMPLE", + "--eqo-sample", + "abab", + STATEFUL}, "/mealy.dot"); + } + + @Test + public void testStatefulDFA() throws IOException { + checkExecution(new String[] {"-sa", "-sb", "--reset", "reset", "-eSAMPLE", "--eqo-sample", "abab", STATEFUL}, + "/dfa.dot"); + } + + @Test + public void testStatefulStdInMealy() throws IOException { + checkExecution(new String[] {"-sa", + "-sb", + "-tMEALY", + "--stdin", + "--reset", + "reset", + "-eSAMPLE", + "--eqo-sample", + "abab", + STATEFUL_STDIN}, "/mealy.dot"); + } + + @Test + public void testStatefulStdInDFA() throws IOException { + checkExecution(new String[] {"-sa", + "-sb", + "--stdin", + "--reset", + "reset", + "-eSAMPLE", + "--eqo-sample", + "abab", + STATEFUL_STDIN}, "/dfa.dot"); + } + + private void checkExecution(String[] params, String resource) throws IOException { + Path out = Files.createTempFile("automatalib", ""); + out.toFile().deleteOnExit(); + + final String[] args = Arrays.copyOf(params, params.length + 2); + 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); + checkOutputs(out, resource); + } + + private 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/resources/dfa.dot b/cli/src/test/resources/dfa.dot new file mode 100644 index 000000000..356623100 --- /dev/null +++ b/cli/src/test/resources/dfa.dot @@ -0,0 +1,13 @@ +digraph g { + + s0 [shape="circle" label="s0"]; + s1 [shape="doublecircle" label="s1"]; + 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/mealy.dot b/cli/src/test/resources/mealy.dot new file mode 100644 index 000000000..89539b79e --- /dev/null +++ b/cli/src/test/resources/mealy.dot @@ -0,0 +1,10 @@ +digraph g { + + s0 [shape="circle" label="s0"]; + 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/suls/stateful.py b/cli/src/test/resources/suls/stateful.py new file mode 100755 index 000000000..5544ac2fb --- /dev/null +++ b/cli/src/test/resources/suls/stateful.py @@ -0,0 +1,28 @@ +#!/bin/python +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/suls/stateful_stdin.py b/cli/src/test/resources/suls/stateful_stdin.py new file mode 100755 index 000000000..277c9caa4 --- /dev/null +++ b/cli/src/test/resources/suls/stateful_stdin.py @@ -0,0 +1,28 @@ +#!/bin/python +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])) + + pickle.dump(argv, open(path, "wb")) + sys.exit(argv % 2) + +if __name__ == "__main__": + main() diff --git a/cli/src/test/resources/suls/stateless.py b/cli/src/test/resources/suls/stateless.py new file mode 100755 index 000000000..eaaa6a480 --- /dev/null +++ b/cli/src/test/resources/suls/stateless.py @@ -0,0 +1,14 @@ +#!/bin/python +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/suls/stateless_stdin.py b/cli/src/test/resources/suls/stateless_stdin.py new file mode 100755 index 000000000..3587023cd --- /dev/null +++ b/cli/src/test/resources/suls/stateless_stdin.py @@ -0,0 +1,15 @@ +#!/bin/python +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])) + + sys.exit(argv % 2) + +if __name__ == "__main__": + main() 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/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/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..ccc308ef9 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 @@ -97,7 +97,10 @@ private boolean answerStatelessQuery(Word prefix, Word suffix) { } try { - return ProcessUtil.invokeProcess(args, LOGGER::debug, LOGGER::warn) == 0; + logInvocation(args); + final int exitCpde = ProcessUtil.invokeProcess(args, LOGGER::debug, LOGGER::warn); + logResult(exitCpde); + return exitCpde == 0; } catch (IOException | InterruptedException e) { LOGGER.warn("Error while invoking process", e); return false; @@ -107,14 +110,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; @@ -133,4 +145,12 @@ static String[] toCommand(List args, T arg) { return result; } + + private static void logInvocation(String[] command) { + LOGGER.debug("Invoking '{}'", (Object) command); + } + + private static void logResult(int exitCpde) { + LOGGER.debug("Exit code '{}'", exitCpde); + } } 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..92ff04c19 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 @@ -38,7 +38,7 @@ * ({@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. * * @param @@ -111,7 +111,9 @@ private D answerStatelessQuery(Word prefix, Word suffix) { final StringJoiner sj = new StringJoiner(System.lineSeparator()); try { + logInvocation(args); ProcessUtil.invokeProcess(args, sj::add, LOGGER::warn); + logResult(sj); return outputTransformer.apply(sj.toString(), prefix.length()); } catch (IOException | InterruptedException e) { throw new IllegalStateException(e); @@ -123,19 +125,34 @@ private D answerStatefulQuery(Word prefix, Word suffix) { 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); + String[] command = CLIOracle.toCommand(commandLine, p); + logInvocation(command); + ProcessUtil.invokeProcess(command, sj::add, LOGGER::warn); } for (I s : suffix) { - ProcessUtil.invokeProcess(CLIOracle.toCommand(commandLine, s), sj::add, LOGGER::warn); + String[] command = CLIOracle.toCommand(commandLine, s); + logInvocation(command); + ProcessUtil.invokeProcess(command, sj::add, LOGGER::warn); } + logResult(sj); return outputTransformer.apply(sj.toString(), prefix.length()); } catch (IOException | InterruptedException e) { throw new IllegalStateException(e); } } + + private static void logInvocation(String[] command) { + LOGGER.debug("Invoking '{}'", (Object) command); + } + + private static void logResult(StringJoiner sj) { + LOGGER.debug("Received output '{}'", sj); + } } 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..3b5ecdbde 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 @@ -84,11 +84,22 @@ public Boolean answerQuery(Word prefix, Word suffix) { } private boolean answerStatelessQuery(Word prefix, Word suffix) { + + final String input; + + // prevent sending "ε" to the process + if (prefix.isEmpty() && suffix.isEmpty()) { + input = ""; + } else { + input = prefix.concat(suffix).toString(); + } + try { - return ProcessUtil.invokeProcess(commandLine, - new StringReader(prefix.concat(suffix).toString()), - LOGGER::debug, - LOGGER::warn) == 0; + logInvocation(commandLine, input); + final int exitCode = + ProcessUtil.invokeProcess(commandLine, new StringReader(input), LOGGER::debug, LOGGER::warn); + logResult(exitCode); + return exitCode == 0; } catch (IOException | InterruptedException e) { LOGGER.warn("Error while invoking process", e); return false; @@ -98,21 +109,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)), LOGGER::debug, LOGGER::warn); + logResult(returnCode); } for (I s : suffix) { + logInvocation(commandLine, s); returnCode = ProcessUtil.invokeProcess(commandLine, new StringReader(Objects.toString(s)), LOGGER::debug, LOGGER::warn); + logResult(returnCode); } return returnCode == 0; @@ -121,4 +138,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 exitCpde) { + LOGGER.debug("Exit code '{}'", exitCpde); + } } 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..00ebe4b7e 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 @@ -98,12 +98,19 @@ public D answerQuery(Word prefix, Word suffix) { private D answerStatelessQuery(Word prefix, Word suffix) { final StringJoiner sj = new StringJoiner(System.lineSeparator()); + final String input; + + // prevent sending "ε" to the process + if (prefix.isEmpty() && suffix.isEmpty()) { + input = ""; + } else { + input = prefix.concat(suffix).toString(); + } try { - ProcessUtil.invokeProcess(commandLine, - new StringReader(prefix.concat(suffix).toString()), - sj::add, - LOGGER::warn); + logInvocation(commandLine, input); + ProcessUtil.invokeProcess(commandLine, new StringReader(input), sj::add, LOGGER::warn); + logResult(sj); return outputTransformer.apply(sj.toString(), prefix.length()); } catch (IOException | InterruptedException e) { throw new IllegalStateException(e); @@ -115,14 +122,19 @@ private D answerStatefulQuery(Word prefix, Word suffix) { 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) { + logInvocation(commandLine, p); ProcessUtil.invokeProcess(commandLine, new StringReader(Objects.toString(p)), sj::add, LOGGER::warn); + logResult(sj); } for (I s : suffix) { + logInvocation(commandLine, s); ProcessUtil.invokeProcess(commandLine, new StringReader(Objects.toString(s)), sj::add, LOGGER::warn); + logResult(sj); } return outputTransformer.apply(sj.toString(), prefix.length()); @@ -130,4 +142,12 @@ private D answerStatefulQuery(Word prefix, Word suffix) { throw new IllegalStateException(e); } } + + private static void logInvocation(List command, Object payload) { + LOGGER.debug("Invoking '{}' with payload '{}'", command, payload); + } + + private static void logResult(StringJoiner sj) { + LOGGER.debug("Received output '{}'", sj); + } } diff --git a/pom.xml b/pom.xml index 8173c29c2..593967dbf 100644 --- a/pom.xml +++ b/pom.xml @@ -256,6 +256,7 @@ limitations under the License. 0.8.15 3.5.0 3.12.0 + 3.3.0 1.0.0 3.28.0 3.3.1 @@ -280,6 +281,7 @@ limitations under the License. 1.11 5.22.0 3.3.0 + 4.7.7 7.26.0 1.0.4 3.8.6 @@ -627,7 +629,7 @@ limitations under the License. test - + org.checkerframework checker-qual @@ -642,12 +644,6 @@ limitations under the License. provided - - org.apache.fury - fury-core - ${fury.version} - - org.kohsuke.metainf-services metainf-services @@ -655,6 +651,14 @@ limitations under the License. provided + + + org.apache.fury + fury-core + ${fury.version} + + + org.slf4j slf4j-api @@ -695,6 +699,12 @@ limitations under the License. ${reactive-streams.version} + + + info.picocli + picocli + ${picocli.version} + @@ -946,6 +956,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 @@ -1299,5 +1314,18 @@ limitations under the License. examples + + + cli + + + + !learnlib.release + + + + cli + + From a66d7f9847cf6fb1698d52f963afef6e1d51c127 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Tue, 21 Jul 2026 10:07:58 +0200 Subject: [PATCH 02/34] update build profiles --- .github/workflows/ci.yml | 4 ++-- cli/pom.xml | 52 +++++++++++++++++++++++----------------- distribution/pom.xml | 2 +- 3 files changed, 33 insertions(+), 25 deletions(-) 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/cli/pom.xml b/cli/pom.xml index 3a2c57d6e..68d81d7b7 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -168,8 +168,6 @@ limitations under the License. org.testng testng - - @@ -187,26 +185,36 @@ limitations under the License. true - - org.apache.maven.plugins - maven-jlink-plugin - - - package - - jlink - - - false - true - true - true - learnlib-cli-${project.version} - learnlib=de.learnlib.cli/de.learnlib.cli.Application - - - - + + + + cli + + + + org.apache.maven.plugins + maven-jlink-plugin + + + package + + jlink + + + false + true + true + true + learnlib-cli-${project.version} + learnlib=de.learnlib.cli/de.learnlib.cli.Application + + + + + + + + diff --git a/distribution/pom.xml b/distribution/pom.xml index edf01ce31..df06ae263 100644 --- a/distribution/pom.xml +++ b/distribution/pom.xml @@ -227,7 +227,7 @@ limitations under the License. - jlink + integration-tests From fb574e9298c61ab041fc8f1fb16085bab8f9e423 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Tue, 21 Jul 2026 10:10:46 +0200 Subject: [PATCH 03/34] github: add worflow for attaching cli artifacts --- .github/workflows/release.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/release.yml 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 From 42dc4de50025dba7ee4b026a9420b9fec14a2b5b Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Tue, 21 Jul 2026 10:30:00 +0200 Subject: [PATCH 04/34] cli: include OS classifiers in artifact name --- cli/pom.xml | 11 ++++++++++- pom.xml | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/cli/pom.xml b/cli/pom.xml index 68d81d7b7..b04eb42bf 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -177,6 +177,14 @@ limitations under the License. true + + + + kr.motd.maven + os-maven-plugin + ${os-plugin.version} + + org.apache.maven.plugins @@ -207,7 +215,8 @@ limitations under the License. true true true - learnlib-cli-${project.version} + ${project.artifactId}-${project.version} + ${project.artifactId}-${project.version}-${os.detected.classifier} learnlib=de.learnlib.cli/de.learnlib.cli.Application diff --git a/pom.xml b/pom.xml index 593967dbf..b01708405 100644 --- a/pom.xml +++ b/pom.xml @@ -258,6 +258,7 @@ limitations under the License. 3.12.0 3.3.0 1.0.0 + 1.7.1 3.28.0 3.3.1 3.5.0 From aebbb344ba778483144f1a181dcb06c9c1670c21 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Tue, 21 Jul 2026 10:30:15 +0200 Subject: [PATCH 05/34] do not include cli module in releases for now --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b01708405..b7c955147 100644 --- a/pom.xml +++ b/pom.xml @@ -857,7 +857,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 From 7ab9ad144e6f2324d1143b6ee59fa84af503d8a4 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Tue, 21 Jul 2026 10:40:29 +0200 Subject: [PATCH 06/34] update docs --- cli/src/main/java/de/learnlib/cli/Application.java | 2 +- cli/src/main/resources/application.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/src/main/java/de/learnlib/cli/Application.java b/cli/src/main/java/de/learnlib/cli/Application.java index 8cd9e640f..47f507a67 100644 --- a/cli/src/main/java/de/learnlib/cli/Application.java +++ b/cli/src/main/java/de/learnlib/cli/Application.java @@ -33,7 +33,7 @@ descriptionHeading = "%nDescription:%n%n", parameterListHeading = "%nParameters:%n", optionListHeading = "%nOptions:%n", - description = "Stores the current contents of the index in a new commit along with a log message from the user describing the changes.") + description = "Runs an active automata learning process by invoking the provided SUL to answer membership queries. You may use the documentation available at https://learnlib.de/learnlib/maven-site/ to obtain additional information on certain components of LearnLib.") public class Application implements Runnable { public static final String PROPERTIES = "application"; diff --git a/cli/src/main/resources/application.properties b/cli/src/main/resources/application.properties index 48b3891c4..dc8614121 100644 --- a/cli/src/main/resources/application.properties +++ b/cli/src/main/resources/application.properties @@ -15,7 +15,7 @@ app.version=${project.version} -option.args="Additional arguments to pass to the SUL. +option.args=Additional arguments to pass to the SUL. option.cache=Use a query cache. option.delim=The string used to split responses of the SUL into individual output symbols. 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}. From 2d8d1508ba5b0923d84638ebd3a2f1c710780399 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Tue, 21 Jul 2026 16:56:41 +0200 Subject: [PATCH 07/34] tidy:pom --- cli/pom.xml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cli/pom.xml b/cli/pom.xml index b04eb42bf..56fd8685f 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -177,14 +177,6 @@ limitations under the License. true - - - - kr.motd.maven - os-maven-plugin - ${os-plugin.version} - - org.apache.maven.plugins @@ -194,6 +186,14 @@ limitations under the License. + + + + kr.motd.maven + os-maven-plugin + ${os-plugin.version} + + From f743669154e82ae77eba014c23595cb3dcd92ac3 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Tue, 21 Jul 2026 16:56:54 +0200 Subject: [PATCH 08/34] add support for SAF serialization --- cli/pom.xml | 4 ++++ .../java/de/learnlib/cli/factory/SerializerFactory.java | 6 +++--- cli/src/main/java/module-info.java | 7 ++++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/cli/pom.xml b/cli/pom.xml index 56fd8685f..5426e75e5 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -145,6 +145,10 @@ limitations under the License. net.automatalib automata-serialization-saf + + net.automatalib + automata-serialization-taf + net.automatalib automata-util diff --git a/cli/src/main/java/de/learnlib/cli/factory/SerializerFactory.java b/cli/src/main/java/de/learnlib/cli/factory/SerializerFactory.java index 039fd8462..82e472ca7 100644 --- a/cli/src/main/java/de/learnlib/cli/factory/SerializerFactory.java +++ b/cli/src/main/java/de/learnlib/cli/factory/SerializerFactory.java @@ -36,6 +36,7 @@ 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; import net.automatalib.ts.simple.SimpleTS; @FunctionalInterface @@ -64,8 +65,7 @@ private static InputModelSerializer> getDFASerializer(Options o case LEARNLIBV2 -> LearnLibV2Serialization.getInstance(); case MATA -> new MataNFAWriter<>(); case SAF -> SAFWriters.dfa(); - // case TAF -> TAFWriters.dfa(); - default -> throw new UnsupportedCombinationException(options); + case TAF -> TAFWriters.dfa(); }; } @@ -73,7 +73,7 @@ private static InputModelSerializer> getDFASerializer(Options o return switch (options.format) { case DOT -> dotAutomatonWriter(); case SAF -> SAFWriters.mealy(DataOutput::writeUTF); - // case TAF -> TAFWriters.mealy(); + case TAF -> TAFWriters.mealy(); default -> throw new UnsupportedCombinationException(options); }; } diff --git a/cli/src/main/java/module-info.java b/cli/src/main/java/module-info.java index c751ee9ad..0e9b287a4 100644 --- a/cli/src/main/java/module-info.java +++ b/cli/src/main/java/module-info.java @@ -37,12 +37,13 @@ requires net.automatalib.api; requires net.automatalib.common.util; requires net.automatalib.core; - requires net.automatalib.serialization.dot; - requires net.automatalib.serialization.saf; - requires net.automatalib.serialization.mata; 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; From def64cd7a5b878301315ec601ccf6dd91c839673 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Thu, 23 Jul 2026 17:09:10 +0200 Subject: [PATCH 09/34] cli: add ADT and LSHARP algorithm * cli oracles still need some rework regarding error handling --- .../algorithm/adt/learner/ADTLearner.java | 17 ++- cli/pom.xml | 8 ++ .../java/de/learnlib/cli/Application.java | 2 +- .../de/learnlib/cli/factory/EQOFactory.java | 41 ++++-- .../learnlib/cli/factory/LearnerFactory.java | 44 ++++--- .../de/learnlib/cli/factory/MQOFactory.java | 61 +++++++-- .../java/de/learnlib/cli/option/Learner.java | 2 + .../java/de/learnlib/cli/option/Options.java | 3 +- .../java/de/learnlib/cli/option/Type.java | 96 +++++++------- .../de/learnlib/cli/util/AbstractRunner.java | 112 +++++++++++++++++ .../de/learnlib/cli/util/AdaptiveRunner.java | 46 +++++++ .../de/learnlib/cli/util/Constructor.java | 21 +++- .../de/learnlib/cli/util/PresetRunner.java | 45 +++++++ .../java/de/learnlib/cli/util/Runner.java | 87 +------------ cli/src/main/java/module-info.java | 2 + .../mealy}/Adaptive2MembershipWrapper.java | 7 +- .../learnlib/oracle/membership/CLIOracle.java | 17 ++- .../membership/CLIOutputAdaptiveOracle.java | 113 +++++++++++++++++ .../oracle/membership/CLIOutputOracle.java | 53 ++++---- .../oracle/membership/StdInOracle.java | 41 +++--- .../membership/StdInOutputAdaptiveOracle.java | 117 ++++++++++++++++++ .../oracle/membership/StdInOutputOracle.java | 62 ++++++---- .../src/main/java/module-info.java | 1 + .../oracle/membership/CLIOracleTest.java | 4 + .../CLIOutputAdaptiveOracleTest.java | 55 ++++++++ .../membership/CLIOutputOracleTest.java | 26 ++-- .../oracle/membership/StdInOracleTest.java | 6 +- .../StdInOutputAdaptiveOracleTest.java | 51 ++++++++ .../membership/StdInOutputOracleTest.java | 21 ++-- .../src/test/resources/stateful_stdin_sul.py | 29 +++++ .../src/test/resources/stateful_sul.py | 24 ++-- .../src/test/resources/stateless_stdin_sul.py | 16 +++ .../src/test/resources/stateless_sul.py | 13 +- 33 files changed, 944 insertions(+), 299 deletions(-) create mode 100644 cli/src/main/java/de/learnlib/cli/util/AbstractRunner.java create mode 100644 cli/src/main/java/de/learnlib/cli/util/AdaptiveRunner.java create mode 100644 cli/src/main/java/de/learnlib/cli/util/PresetRunner.java rename {algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner => commons/util/src/main/java/de/learnlib/util/mealy}/Adaptive2MembershipWrapper.java (87%) create mode 100644 oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/CLIOutputAdaptiveOracle.java create mode 100644 oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/StdInOutputAdaptiveOracle.java create mode 100644 oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/CLIOutputAdaptiveOracleTest.java create mode 100644 oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/StdInOutputAdaptiveOracleTest.java create mode 100755 oracles/membership-oracles/src/test/resources/stateful_stdin_sul.py create mode 100755 oracles/membership-oracles/src/test/resources/stateless_stdin_sul.py 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/cli/pom.xml b/cli/pom.xml index 5426e75e5..f25c3ba1d 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -59,6 +59,10 @@ limitations under the License. + + de.learnlib + learnlib-adt + de.learnlib learnlib-dhc @@ -75,6 +79,10 @@ limitations under the License. de.learnlib learnlib-lambda + + de.learnlib + learnlib-lsharp + de.learnlib learnlib-lstar diff --git a/cli/src/main/java/de/learnlib/cli/Application.java b/cli/src/main/java/de/learnlib/cli/Application.java index 47f507a67..83faaf4d4 100644 --- a/cli/src/main/java/de/learnlib/cli/Application.java +++ b/cli/src/main/java/de/learnlib/cli/Application.java @@ -50,7 +50,7 @@ public static void main(String[] args) { @Override public void run() { setLogLevel(options); - options.type.runner().run(options); + options.type.runner(options).run(options); } private void setLogLevel(Options options) { diff --git a/cli/src/main/java/de/learnlib/cli/factory/EQOFactory.java b/cli/src/main/java/de/learnlib/cli/factory/EQOFactory.java index e836362dd..03f58daf7 100644 --- a/cli/src/main/java/de/learnlib/cli/factory/EQOFactory.java +++ b/cli/src/main/java/de/learnlib/cli/factory/EQOFactory.java @@ -22,6 +22,7 @@ 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; @@ -34,8 +35,9 @@ 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; +import net.automatalib.automaton.UniversalDeterministicAutomaton.RegularAutomaton; import net.automatalib.automaton.concept.SuffixOutput; import net.automatalib.automaton.fsa.DFA; import net.automatalib.automaton.fsa.NFA; @@ -48,17 +50,26 @@ import net.automatalib.word.Word; @FunctionalInterface -public interface EQOFactory extends BiFunction, EquivalenceOracle> { - - EQOFactory, String, Boolean> DFA_ORACLES = EQOFactory::getRegularOracles; - EQOFactory, String, Word> MEALY_ORACLES = EQOFactory::getRegularOracles; - EQOFactory, String, Boolean> NFA_ORACLES = EQOFactory::getNFAOracles; - EQOFactory, String, Boolean> SBA_ORACLES = EQOFactory::getSBAOracles; - EQOFactory, String, Boolean> SPA_ORACLES = EQOFactory::getSPAOracles; - EQOFactory, String, Word> SPMM_ORACLES = EQOFactory::getSPMMOracles; - EQOFactory, String, Boolean> VPA_ORACLES = EQOFactory::getVPAOracles; - - private static & SuffixOutput, D> EQOracleChain getRegularOracles( +public interface EQOFactory extends BiFunction> { + + EQOFactory, String, Boolean, MembershipOracle> DFA_ORACLES = + EQOFactory::getRegularOracles; + EQOFactory, String, Word, MembershipOracle>> + MEALY_ORACLES = EQOFactory::getRegularOracles; + EQOFactory, String, Word, AdaptiveMembershipOracle> + ADAPTIVE_ORACLES = EQOFactory::getAdaptiveOracles; + EQOFactory, String, Boolean, MembershipOracle> NFA_ORACLES = + EQOFactory::getNFAOracles; + EQOFactory, String, Boolean, MembershipOracle> SBA_ORACLES = + EQOFactory::getSBAOracles; + EQOFactory, String, Boolean, MembershipOracle> SPA_ORACLES = + EQOFactory::getSPAOracles; + EQOFactory, String, Word, MembershipOracle>> SPMM_ORACLES = + EQOFactory::getSPMMOracles; + EQOFactory, String, Boolean, MembershipOracle> VPA_ORACLES = + EQOFactory::getVPAOracles; + + private static & SuffixOutput, D> EQOracleChain getRegularOracles( Options options, MembershipOracle mqo) { final EQOracleChain chain = new EQOracleChain<>(); @@ -112,6 +123,12 @@ public interface EQOFactory extends BiFunction & SuffixOutput>, O> EQOracleChain> getAdaptiveOracles( + Options options, + AdaptiveMembershipOracle mqo) { + return getRegularOracles(options, new Adaptive2MembershipWrapper<>(mqo)); + } + private static EquivalenceOracle, String, Boolean> getNFAOracles(Options options, MembershipOracle mqo) { final EQOracleChain, String, Boolean> chain = getRegularOracles(options, mqo); diff --git a/cli/src/main/java/de/learnlib/cli/factory/LearnerFactory.java b/cli/src/main/java/de/learnlib/cli/factory/LearnerFactory.java index a9dc89357..7e310dafe 100644 --- a/cli/src/main/java/de/learnlib/cli/factory/LearnerFactory.java +++ b/cli/src/main/java/de/learnlib/cli/factory/LearnerFactory.java @@ -17,6 +17,8 @@ import java.util.function.Function; +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; @@ -41,8 +43,12 @@ import de.learnlib.cli.option.Learner; import de.learnlib.cli.option.Options; import de.learnlib.cli.util.Constructor; +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.oracle.AdaptiveMembershipOracle; +import de.learnlib.oracle.MembershipOracle; import net.automatalib.alphabet.Alphabet; import net.automatalib.alphabet.ProceduralInputAlphabet; import net.automatalib.alphabet.VPAlphabet; @@ -57,20 +63,22 @@ import net.automatalib.word.Word; @FunctionalInterface -public interface LearnerFactory, M extends FiniteRepresentation, D> - extends Function> { +public interface LearnerFactory, M extends FiniteRepresentation, D, OR> + extends Function> { - LearnerFactory, DFA, Boolean> DFA_LEARNER = LearnerFactory::getDFALearner; - LearnerFactory, MealyMachine, Word> MEALY_LEARNER = + LearnerFactory, DFA, Boolean, MembershipOracle> DFA_LEARNER = LearnerFactory::getDFALearner; + LearnerFactory, MealyMachine, Word, MembershipOracle>> MEALY_LEARNER = LearnerFactory::getMealyLearner; - LearnerFactory, NFA, Boolean> NFA_LEARNER = LearnerFactory::getNFALearner; - LearnerFactory, SBA, Boolean> SBA_LEARNER = + LearnerFactory, MealyMachine, Word, AdaptiveMembershipOracle> ADAPTIVE_LEARNER = + LearnerFactory::getAdaptiveLearner; + LearnerFactory, NFA, Boolean, MembershipOracle> NFA_LEARNER = LearnerFactory::getNFALearner; + LearnerFactory, SBA, Boolean, MembershipOracle> SBA_LEARNER = LearnerFactory::getSBALearner; - LearnerFactory, SPA, Boolean> SPA_LEARNER = + LearnerFactory, SPA, Boolean, MembershipOracle> SPA_LEARNER = LearnerFactory::getSPALearner; - LearnerFactory, SPMM, Word> SPMM_LEARNER = + LearnerFactory, SPMM, Word, MembershipOracle>> SPMM_LEARNER = LearnerFactory::getSPMMLearner; - LearnerFactory, OneSEVPA, Boolean> VPA_LEARNER = LearnerFactory::getVPALearner; + LearnerFactory, OneSEVPA, Boolean, MembershipOracle> VPA_LEARNER = LearnerFactory::getVPALearner; private static DFAConstructor, I> getDFALearner(Options options) { return switch (options.learner) { @@ -98,30 +106,38 @@ private static MealyConstructor, I, O> getMealyLearner(Option }; } - private static Constructor, NFA, I, Boolean> getNFALearner(Options options) { + private static AdaptiveConstructor, MealyMachine, I, O> getAdaptiveLearner(Options options) { + return switch (options.learner) { + case ADT -> ADTLearner::new; + case LSHARP -> LSharpMealy::new; + default -> throw new UnsupportedCombinationException(options); + }; + } + + private static PresetConstructor, NFA, I, Boolean> getNFALearner(Options options) { if (options.learner == Learner.NLSTAR) { return NLStarLearner::new; } throw new UnsupportedCombinationException(options); } - private static Constructor, SBA, I, Boolean> getSBALearner(Options options) { + private static PresetConstructor, SBA, I, Boolean> getSBALearner(Options options) { final DFAConstructor>, SymbolWrapper> learner = getDFALearner(options); return (alph, mqo) -> new SBALearner<>(alph, mqo, learner::constructLearner); } - private static Constructor, SPA, I, Boolean> getSPALearner(Options options) { + private static PresetConstructor, SPA, I, Boolean> getSPALearner(Options options) { final DFAConstructor, I> learner = getDFALearner(options); return (alph, mqo) -> new SPALearner<>(alph, mqo, learner::constructLearner); } - private static Constructor, SPMM, I, Word> getSPMMLearner( + private 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); } - private static Constructor, OneSEVPA, I, Boolean> getVPALearner(Options options) { + private static PresetConstructor, OneSEVPA, I, Boolean> getVPALearner(Options options) { return switch (options.learner) { case OP -> (alphabet, mqo) -> new OPLearnerVPABuilder().withAlphabet(alphabet).withOracle(mqo).create(); case TTT -> diff --git a/cli/src/main/java/de/learnlib/cli/factory/MQOFactory.java b/cli/src/main/java/de/learnlib/cli/factory/MQOFactory.java index fe18c9374..481563ea8 100644 --- a/cli/src/main/java/de/learnlib/cli/factory/MQOFactory.java +++ b/cli/src/main/java/de/learnlib/cli/factory/MQOFactory.java @@ -25,21 +25,27 @@ 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 net.automatalib.alphabet.Alphabet; import net.automatalib.word.Word; @FunctionalInterface -public interface MQOFactory extends Function> { +public interface MQOFactory extends BiFunction { - MQOFactory ACCEPTOR = MQOFactory::getAcceptorOracle; - MQOFactory> TRANSDUCER = MQOFactory::getTransducerOracle; + MQOFactory, Alphabet> ACCEPTOR = MQOFactory::getAcceptorOracle; + MQOFactory>, Alphabet> TRANSDUCER = MQOFactory::getTransducerOracle; + MQOFactory, Alphabet> ADAPTIVE = MQOFactory::getAdaptiveOracle; - private static MembershipOracle getAcceptorOracle(Options options) { + private static MembershipOracle getAcceptorOracle(Options options, Alphabet alphabet) { verifyPath(options); MembershipOracle oracle; @@ -56,7 +62,7 @@ private static MembershipOracle getAcceptorOracle(Options optio } if (options.cache) { - oracle = DFACaches.createHashCache(oracle); + oracle = DFACaches.createDAGCache(alphabet, oracle); if (options.statistics) { oracle = new CounterOracle<>(oracle, "cache"); } @@ -65,7 +71,8 @@ private static MembershipOracle getAcceptorOracle(Options optio return oracle; } - private static MembershipOracle> getTransducerOracle(Options options) { + private static MembershipOracle> getTransducerOracle(Options options, + Alphabet alphabet) { verifyPath(options); MembershipOracle> oracle; @@ -82,7 +89,7 @@ private static MembershipOracle> getTransducerOracle(Option } if (options.cache) { - oracle = MealyCaches.createDynamicTreeCache(oracle); + oracle = MealyCaches.createDAGCache(alphabet, oracle); if (options.statistics) { oracle = new CounterOracle<>(oracle, "cache"); } @@ -91,6 +98,34 @@ private static MembershipOracle> getTransducerOracle(Option return oracle; } + private static AdaptiveMembershipOracle getAdaptiveOracle(Options options, + Alphabet alphabet) { + verifyPath(options); + verifyReset(options); + + AdaptiveMembershipOracle oracle; + + if (options.stdin) { + oracle = new StdInOutputAdaptiveOracle<>(buildCommandLine(options), l -> l.get(0), options.reset); + } else { + oracle = new CLIOutputAdaptiveOracle<>(buildCommandLine(options), l -> l.get(0), options.reset); + + } + + if (options.statistics) { + oracle = new CounterAdaptiveQueryOracle<>(oracle, "sul"); + } + + if (options.cache) { + oracle = MealyCaches.createAdaptiveQueryCache(alphabet, oracle); + if (options.statistics) { + oracle = new CounterAdaptiveQueryOracle<>(oracle, "cache"); + } + } + + return oracle; + } + private static List buildCommandLine(Options options) { final String absolutePath = options.sul.getAbsolutePath(); if (options.additionalArgs == null) { @@ -109,7 +144,15 @@ private static void verifyPath(Options options) { } } - class OutputTransformer implements BiFunction> { + 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)); + } + } + + class OutputTransformer implements BiFunction, Integer, Word> { private final Pattern pattern; @@ -118,7 +161,7 @@ public OutputTransformer(Options options) { } @Override - public Word apply(String string, Integer offset) { + public Word apply(List string, Integer offset) { if (string.isEmpty()) { return Word.epsilon(); } diff --git a/cli/src/main/java/de/learnlib/cli/option/Learner.java b/cli/src/main/java/de/learnlib/cli/option/Learner.java index 36744a865..ebf2ff340 100644 --- a/cli/src/main/java/de/learnlib/cli/option/Learner.java +++ b/cli/src/main/java/de/learnlib/cli/option/Learner.java @@ -17,9 +17,11 @@ public enum Learner { + ADT, DHC, KV, LLAMBDA, + LSHARP, LSTAR, NLSTAR, OP, diff --git a/cli/src/main/java/de/learnlib/cli/option/Options.java b/cli/src/main/java/de/learnlib/cli/option/Options.java index abe994a1f..371beda93 100644 --- a/cli/src/main/java/de/learnlib/cli/option/Options.java +++ b/cli/src/main/java/de/learnlib/cli/option/Options.java @@ -16,6 +16,7 @@ package de.learnlib.cli.option; import java.io.File; +import java.nio.file.Path; import java.util.List; import picocli.CommandLine.ArgGroup; @@ -58,7 +59,7 @@ public class Options { public Output format; @Option(names = {"-o", "--output"}, paramLabel = "", descriptionKey = "option.output") - public File output; + public Path output; @Option(names = {"-v", "--verbose"}, descriptionKey = "option.verbose") public boolean[] verbosity; diff --git a/cli/src/main/java/de/learnlib/cli/option/Type.java b/cli/src/main/java/de/learnlib/cli/option/Type.java index 910736090..b55fcadeb 100644 --- a/cli/src/main/java/de/learnlib/cli/option/Type.java +++ b/cli/src/main/java/de/learnlib/cli/option/Type.java @@ -20,80 +20,90 @@ 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() { - return new Runner<>(AlphabetFactory.REGULAR, - MQOFactory.ACCEPTOR, - LearnerFactory.DFA_LEARNER, - EQOFactory.DFA_ORACLES, - SerializerFactory.DFA_SERIALIZER); + public Runner runner(Options options) { + return new PresetRunner<>(AlphabetFactory.REGULAR, + MQOFactory.ACCEPTOR, + LearnerFactory.DFA_LEARNER, + EQOFactory.DFA_ORACLES, + SerializerFactory.DFA_SERIALIZER); } }, MEALY { @Override - public Runner runner() { - return new Runner<>(AlphabetFactory.REGULAR, - MQOFactory.TRANSDUCER, - LearnerFactory.MEALY_LEARNER, - EQOFactory.MEALY_ORACLES, - SerializerFactory.MEALY_SERIALIZER); + public Runner runner(Options options) { + if (options.learner == Learner.ADT || options.learner == Learner.LSHARP) { + return new AdaptiveRunner<>(AlphabetFactory.REGULAR, + MQOFactory.ADAPTIVE, + LearnerFactory.ADAPTIVE_LEARNER, + EQOFactory.ADAPTIVE_ORACLES, + SerializerFactory.MEALY_SERIALIZER); + } else { + return new PresetRunner<>(AlphabetFactory.REGULAR, + MQOFactory.TRANSDUCER, + LearnerFactory.MEALY_LEARNER, + EQOFactory.MEALY_ORACLES, + SerializerFactory.MEALY_SERIALIZER); + } } }, NFA { @Override - public Runner runner() { - return new Runner<>(AlphabetFactory.REGULAR, - MQOFactory.ACCEPTOR, - LearnerFactory.NFA_LEARNER, - EQOFactory.NFA_ORACLES, - SerializerFactory.NFA_SERIALIZER); + public Runner runner(Options options) { + return new PresetRunner<>(AlphabetFactory.REGULAR, + MQOFactory.ACCEPTOR, + LearnerFactory.NFA_LEARNER, + EQOFactory.NFA_ORACLES, + SerializerFactory.NFA_SERIALIZER); } }, SBA { @Override - public Runner runner() { - return new Runner<>(AlphabetFactory.PROCEDURAL, - MQOFactory.ACCEPTOR, - LearnerFactory.SBA_LEARNER, - EQOFactory.SBA_ORACLES, - SerializerFactory.SBA_SERIALIZER); + public Runner runner(Options options) { + return new PresetRunner<>(AlphabetFactory.PROCEDURAL, + MQOFactory.ACCEPTOR, + LearnerFactory.SBA_LEARNER, + EQOFactory.SBA_ORACLES, + SerializerFactory.SBA_SERIALIZER); } }, SPA { @Override - public Runner runner() { - return new Runner<>(AlphabetFactory.PROCEDURAL, - MQOFactory.ACCEPTOR, - LearnerFactory.SPA_LEARNER, - EQOFactory.SPA_ORACLES, - SerializerFactory.SPA_SERIALIZER); + public Runner runner(Options options) { + return new PresetRunner<>(AlphabetFactory.PROCEDURAL, + MQOFactory.ACCEPTOR, + LearnerFactory.SPA_LEARNER, + EQOFactory.SPA_ORACLES, + SerializerFactory.SPA_SERIALIZER); } }, SPMM { @Override - public Runner runner() { - return new Runner<>(AlphabetFactory.PROCEDURAL, - MQOFactory.TRANSDUCER, - LearnerFactory.SPMM_LEARNER, - EQOFactory.SPMM_ORACLES, - SerializerFactory.SPMM_SERIALIZER); + public Runner runner(Options options) { + return new PresetRunner<>(AlphabetFactory.PROCEDURAL, + MQOFactory.TRANSDUCER, + LearnerFactory.SPMM_LEARNER, + EQOFactory.SPMM_ORACLES, + SerializerFactory.SPMM_SERIALIZER); } }, VPA { @Override - public Runner runner() { - return new Runner<>(AlphabetFactory.VPA, - MQOFactory.ACCEPTOR, - LearnerFactory.VPA_LEARNER, - EQOFactory.VPA_ORACLES, - SerializerFactory.VPA_SERIALIZER); + public Runner runner(Options options) { + return new PresetRunner<>(AlphabetFactory.VPA, + MQOFactory.ACCEPTOR, + LearnerFactory.VPA_LEARNER, + EQOFactory.VPA_ORACLES, + SerializerFactory.VPA_SERIALIZER); } }; - public abstract Runner runner(); + 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..43b7a961e --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/util/AbstractRunner.java @@ -0,0 +1,112 @@ +/* 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.algorithm.LearningAlgorithm; +import de.learnlib.cli.option.Options; +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); + + 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 + @SuppressWarnings("PMD.AvoidThrowingRawExceptionTypes") + 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"); + } else { + learnerOracle = mqo; + } + + final LearningAlgorithm learner = + learnerCreator.apply(options).constructLearner(alphabet, learnerOracle); + + final OR eqoOracle; + if (options.statistics) { + eqoOracle = getCounter(mqo, "eqo"); + } else { + eqoOracle = mqo; + } + + final EquivalenceOracle eqo = eqoCreator.apply(options, eqoOracle); + final InputModelSerializer serializer = serializerCreator.apply(options); + + final Experiment experiment = new Experiment<>(learner, eqo, alphabet); + experiment.run(); + + final M hyp = experiment.getFinalHypothesis(); + + if (options.statistics) { + final StatisticsService service = Statistics.getService(); + LOGGER.info(service.print()); + service.clear(); + } + + try { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + serializer.writeModel(baos, hyp, alphabet); + + LOGGER.info("Hypothesis:\n{}", baos.toString(StandardCharsets.UTF_8)); + if (options.output != null) { + Files.write(options.output, baos.toByteArray()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + 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 index d9e8fd71a..d19a8887e 100644 --- a/cli/src/main/java/de/learnlib/cli/util/Constructor.java +++ b/cli/src/main/java/de/learnlib/cli/util/Constructor.java @@ -18,6 +18,7 @@ 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; @@ -25,20 +26,32 @@ import net.automatalib.word.Word; @FunctionalInterface -public interface Constructor, M, I, D> { +public interface Constructor, M, I, D, OR> { - LearningAlgorithm constructLearner(A alphabet, MembershipOracle oracle); + 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 Constructor, I, Word> { + extends PresetConstructor, I, Word> { @Override ProceduralMealyAdapter constructLearner(A alphabet, MembershipOracle> oracle); } @FunctionalInterface - interface DFAConstructor, I> extends Constructor, I, Boolean> { + 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 index bc6f82cf8..910aac55d 100644 --- a/cli/src/main/java/de/learnlib/cli/util/Runner.java +++ b/cli/src/main/java/de/learnlib/cli/util/Runner.java @@ -15,92 +15,11 @@ */ package de.learnlib.cli.util; -import java.io.IOException; -import java.util.function.BiFunction; -import java.util.function.Function; - -import de.learnlib.algorithm.LearningAlgorithm; import de.learnlib.cli.option.Options; -import de.learnlib.filter.statistic.oracle.CounterOracle; -import de.learnlib.oracle.EquivalenceOracle; -import de.learnlib.oracle.MembershipOracle; -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 class Runner, M extends SimpleTS & FiniteRepresentation, I, D> { - - private static final Logger LOGGER = LoggerFactory.getLogger(Runner.class); - - private final Function alphabetCreator; - private final Function> mqoCreator; - private final Function> learnerCreator; - private final BiFunction, EquivalenceOracle> eqoCreator; - private final Function> serializerCreator; - - public Runner(Function alphabetCreator, - Function> mqoCreator, - Function> learnerCreator, - BiFunction, EquivalenceOracle> eqoCreator, - Function> serializerCreator) { - this.alphabetCreator = alphabetCreator; - this.mqoCreator = mqoCreator; - this.learnerCreator = learnerCreator; - this.eqoCreator = eqoCreator; - this.serializerCreator = serializerCreator; - } - - @SuppressWarnings("PMD.AvoidThrowingRawExceptionTypes") - public void run(Options options) { - - final A alphabet = alphabetCreator.apply(options); - final MembershipOracle mqo = mqoCreator.apply(options); - - final MembershipOracle learnerOracle; - if (options.statistics) { - learnerOracle = new CounterOracle<>(mqo, "learner"); - } else { - learnerOracle = mqo; - } - - final LearningAlgorithm learner = - learnerCreator.apply(options).constructLearner(alphabet, learnerOracle); - - final MembershipOracle eqoOracle; - if (options.statistics) { - eqoOracle = new CounterOracle<>(mqo, "eqo"); - } else { - eqoOracle = mqo; - } - - final EquivalenceOracle eqo = eqoCreator.apply(options, eqoOracle); - final InputModelSerializer serializer = serializerCreator.apply(options); - - final Experiment experiment = new Experiment<>(learner, eqo, alphabet); - experiment.run(); - final M hyp = experiment.getFinalHypothesis(); +@FunctionalInterface +public interface Runner { - if (options.statistics) { - StatisticsService service = Statistics.getService(); - LOGGER.info(service.print()); - service.clear(); - } + void run(Options options); - try { - if (options.output != null) { - serializer.writeModel(options.output, hyp, alphabet); - } else { - serializer.writeModel(System.out, hyp, alphabet); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } } diff --git a/cli/src/main/java/module-info.java b/cli/src/main/java/module-info.java index 0e9b287a4..48eb13290 100644 --- a/cli/src/main/java/module-info.java +++ b/cli/src/main/java/module-info.java @@ -15,9 +15,11 @@ */ 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; 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/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 ccc308ef9..2d39507d5 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; @@ -89,18 +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 { logInvocation(args); - final int exitCpde = ProcessUtil.invokeProcess(args, LOGGER::debug, LOGGER::warn); - logResult(exitCpde); - return exitCpde == 0; + 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; @@ -141,7 +140,7 @@ 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; } @@ -150,7 +149,7 @@ private static void logInvocation(String[] command) { LOGGER.debug("Invoking '{}'", (Object) command); } - private static void logResult(int exitCpde) { - LOGGER.debug("Exit code '{}'", exitCpde); + 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..c07d46de5 --- /dev/null +++ b/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/CLIOutputAdaptiveOracle.java @@ -0,0 +1,113 @@ +/* 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.io.StringWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +import de.learnlib.oracle.SingleAdaptiveMembershipOracle; +import de.learnlib.query.AdaptiveQuery; +import de.learnlib.query.AdaptiveQuery.Response; +import net.automatalib.common.util.process.ProcessUtil; +import net.automatalib.word.Word; +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 #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 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. + * + * @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, O> 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 process output (stdin and stderr) as well as + * the length of the query prefix for properly offsetting potentially {@link Word}-based output types. + * @param reset + * the symbol passed to the program to indicate a reset + */ + public CLIOutputAdaptiveOracle(List commandLine, Function, O> 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 { + final List list = new ArrayList<>(); + final I input = query.getInput(); + + final String[] command = CLIOracle.toCommand(commandLine, input); + + logInvocation(command); + ProcessUtil.invokeProcess(command, list::add, LOGGER::warn); + logResult(list); + + final O output = outputTransformer.apply(list); + 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(List 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 92ff04c19..47f34ca41 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 @@ -16,6 +16,8 @@ package de.learnlib.oracle.membership; import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.StringJoiner; @@ -51,7 +53,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 BiFunction, Integer, D> outputTransformer; private final @Nullable String reset; /** @@ -60,12 +62,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 for properly offsetting potentially {@link Word}-based output types. * * @see #CLIOutputOracle(List, BiFunction, String) */ - public CLIOutputOracle(List commandLine, BiFunction outputTransformer) { + public CLIOutputOracle(List commandLine, BiFunction, Integer, D> outputTransformer) { this(commandLine, outputTransformer, null); } @@ -75,14 +77,14 @@ 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 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) { + BiFunction, Integer, D> outputTransformer, + String reset) { this.commandLine = commandLine; this.reset = reset; this.outputTransformer = outputTransformer; @@ -108,13 +110,13 @@ private D answerStatelessQuery(Word prefix, Word suffix) { args[idx++] = Objects.toString(s); } - final StringJoiner sj = new StringJoiner(System.lineSeparator()); + final List output = new ArrayList<>(); try { logInvocation(args); - ProcessUtil.invokeProcess(args, sj::add, LOGGER::warn); - logResult(sj); - return outputTransformer.apply(sj.toString(), prefix.length()); + ProcessUtil.invokeProcess(args, output::add, LOGGER::warn); + logResult(output); + return outputTransformer.apply(output, prefix.length()); } catch (IOException | InterruptedException e) { throw new IllegalStateException(e); } @@ -122,7 +124,7 @@ private D answerStatelessQuery(Word prefix, Word suffix) { @RequiresNonNull("this.reset") private D answerStatefulQuery(Word prefix, Word suffix) { - final StringJoiner sj = new StringJoiner(System.lineSeparator()); + final List output = new ArrayList<>(); try { final String[] resetCommand = CLIOracle.toCommand(commandLine, reset); @@ -130,29 +132,34 @@ private D answerStatefulQuery(Word prefix, Word suffix) { ProcessUtil.invokeProcess(resetCommand, LOGGER::debug, LOGGER::warn); for (I p : prefix) { - String[] command = CLIOracle.toCommand(commandLine, p); - logInvocation(command); - ProcessUtil.invokeProcess(command, sj::add, LOGGER::warn); + answerStatefulSymbol(p, output); } for (I s : suffix) { - String[] command = CLIOracle.toCommand(commandLine, s); - logInvocation(command); - ProcessUtil.invokeProcess(command, sj::add, LOGGER::warn); + answerStatefulSymbol(s, output); } - logResult(sj); - return outputTransformer.apply(sj.toString(), prefix.length()); + return outputTransformer.apply(output, prefix.length()); } catch (IOException | InterruptedException e) { throw new IllegalStateException(e); } } + private void answerStatefulSymbol(I i, List output) throws IOException, InterruptedException { + // ProcessUtil calls the stdout consumer for every line, so replicate the newlines in the output + final StringJoiner sj = new StringJoiner(System.lineSeparator()); + String[] command = CLIOracle.toCommand(commandLine, i); + logInvocation(command); + ProcessUtil.invokeProcess(command, sj::add, LOGGER::warn); + logResult(sj); + output.add(sj.toString()); + } + private static void logInvocation(String[] command) { LOGGER.debug("Invoking '{}'", (Object) command); } - private static void logResult(StringJoiner sj) { - LOGGER.debug("Received output '{}'", sj); + private static void logResult(Object output) { + LOGGER.debug("Received output '{}'", output); } } 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 3b5ecdbde..fd2048b0a 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,10 +35,9 @@ * 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 Word#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 @@ -49,6 +50,8 @@ public class StdInOracle implements SingleQueryOracle { private static final Logger LOGGER = LoggerFactory.getLogger(StdInOracle.class); + private static final String DELIM = + AutomataLibSettings.getInstance().getProperty(AutomataLibProperty.WORD_SYMBOL_SEPARATOR, " "); private final List commandLine; private final @Nullable String reset; @@ -84,20 +87,22 @@ public Boolean answerQuery(Word prefix, Word suffix) { } private boolean answerStatelessQuery(Word prefix, Word suffix) { + final StringJoiner sj = new StringJoiner(DELIM); - final String input; + for (I p : prefix) { + sj.add(String.valueOf(p)); + } - // prevent sending "ε" to the process - if (prefix.isEmpty() && suffix.isEmpty()) { - input = ""; - } else { - input = prefix.concat(suffix).toString(); + for (I s : suffix) { + sj.add(String.valueOf(s)); } try { - logInvocation(commandLine, input); - final int exitCode = - ProcessUtil.invokeProcess(commandLine, new StringReader(input), LOGGER::debug, LOGGER::warn); + 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) { @@ -117,7 +122,7 @@ private boolean answerStatefulQuery(Word prefix, Word suffix) { 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); @@ -126,7 +131,7 @@ private boolean answerStatefulQuery(Word prefix, Word suffix) { 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); @@ -143,7 +148,7 @@ private static void logInvocation(List command, Object payload) { LOGGER.debug("Invoking '{}' with payload '{}'", command, payload); } - private static void logResult(int exitCpde) { - LOGGER.debug("Exit code '{}'", exitCpde); + 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..112f8f3a7 --- /dev/null +++ b/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/StdInOutputAdaptiveOracle.java @@ -0,0 +1,117 @@ +/* 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.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; + +import de.learnlib.oracle.SingleAdaptiveMembershipOracle; +import de.learnlib.query.AdaptiveQuery; +import de.learnlib.query.AdaptiveQuery.Response; +import net.automatalib.common.util.process.ProcessUtil; +import net.automatalib.word.Word; +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 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. + *

+ * 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. + * + * @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, O> 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 as well as the length of the + * query prefix for properly offsetting potentially {@link Word}-based output types. + * @param reset + * the symbol passed to the program to indicate a reset + */ + public StdInOutputAdaptiveOracle(List commandLine, + Function, O> 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 { + final List list = new ArrayList<>(); + final I input = query.getInput(); + + logInvocation(commandLine, input); + ProcessUtil.invokeProcess(commandLine, + new StringReader(Objects.toString(input)), + list::add, + LOGGER::warn); + logResult(list); + + final O output = outputTransformer.apply(list); + 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(List 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 00ebe4b7e..4a16a7f39 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 @@ -17,12 +17,14 @@ import java.io.IOException; import java.io.StringReader; +import java.util.ArrayList; 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; @@ -52,9 +54,11 @@ public class StdInOutputOracle implements SingleQueryOracle { private static final Logger LOGGER = LoggerFactory.getLogger(StdInOutputOracle.class); + private static final String DELIM = + AutomataLibSettings.getInstance().getProperty(AutomataLibProperty.WORD_SYMBOL_SEPARATOR, " "); private final List commandLine; - private final BiFunction outputTransformer; + private final BiFunction, Integer, D> outputTransformer; private final @Nullable String reset; /** @@ -68,7 +72,7 @@ public class StdInOutputOracle implements SingleQueryOracle { * * @see #StdInOutputOracle(List, BiFunction, String) */ - public StdInOutputOracle(List commandLine, BiFunction outputTransformer) { + public StdInOutputOracle(List commandLine, BiFunction, Integer, D> outputTransformer) { this(commandLine, outputTransformer, null); } @@ -84,8 +88,8 @@ public StdInOutputOracle(List commandLine, BiFunction commandLine, - BiFunction outputTransformer, - @Nullable String reset) { + BiFunction, Integer, D> outputTransformer, + String reset) { this.commandLine = commandLine; this.outputTransformer = outputTransformer; this.reset = reset; @@ -97,21 +101,24 @@ public D answerQuery(Word prefix, Word suffix) { } private D answerStatelessQuery(Word prefix, Word suffix) { - final StringJoiner sj = new StringJoiner(System.lineSeparator()); - final String input; + final StringJoiner input = new StringJoiner(DELIM); + + for (I p : prefix) { + input.add(String.valueOf(p)); + } - // prevent sending "ε" to the process - if (prefix.isEmpty() && suffix.isEmpty()) { - input = ""; - } else { - input = prefix.concat(suffix).toString(); + for (I s : suffix) { + input.add(String.valueOf(s)); } try { + final List output = new ArrayList<>(); + logInvocation(commandLine, input); - ProcessUtil.invokeProcess(commandLine, new StringReader(input), sj::add, LOGGER::warn); - logResult(sj); - return outputTransformer.apply(sj.toString(), prefix.length()); + ProcessUtil.invokeProcess(commandLine, new StringReader(input.toString()), output::add, LOGGER::warn); + logResult(output); + + return outputTransformer.apply(output, prefix.length()); } catch (IOException | InterruptedException e) { throw new IllegalStateException(e); } @@ -119,35 +126,40 @@ private D answerStatelessQuery(Word prefix, Word suffix) { @RequiresNonNull("this.reset") private D answerStatefulQuery(Word prefix, Word suffix) { - final StringJoiner sj = new StringJoiner(System.lineSeparator()); + final List output = new ArrayList<>(); try { logInvocation(commandLine, reset); ProcessUtil.invokeProcess(commandLine, new StringReader(reset), LOGGER::debug, LOGGER::warn); for (I p : prefix) { - logInvocation(commandLine, p); - ProcessUtil.invokeProcess(commandLine, new StringReader(Objects.toString(p)), sj::add, LOGGER::warn); - logResult(sj); + answerStatefulSymbol(p, output); } for (I s : suffix) { - logInvocation(commandLine, s); - ProcessUtil.invokeProcess(commandLine, new StringReader(Objects.toString(s)), sj::add, LOGGER::warn); - logResult(sj); + answerStatefulSymbol(s, output); } - return outputTransformer.apply(sj.toString(), prefix.length()); + return outputTransformer.apply(output, prefix.length()); } catch (IOException | InterruptedException e) { throw new IllegalStateException(e); } } + private void answerStatefulSymbol(I i, List output) throws IOException, InterruptedException { + // ProcessUtil calls the stdout consumer for every line, so replicate the newlines in the output + final StringJoiner sj = new StringJoiner(System.lineSeparator()); + logInvocation(commandLine, i); + ProcessUtil.invokeProcess(commandLine, new StringReader(String.valueOf(i)), sj::add, LOGGER::warn); + logResult(sj); + output.add(sj.toString()); + } + private static void logInvocation(List command, Object payload) { LOGGER.debug("Invoking '{}' with payload '{}'", command, payload); } - private static void logResult(StringJoiner sj) { - LOGGER.debug("Received output '{}'", sj); + private static void logResult(Object output) { + LOGGER.debug("Received output '{}'", output); } } diff --git a/oracles/membership-oracles/src/main/java/module-info.java b/oracles/membership-oracles/src/main/java/module-info.java index a41940b9a..f673055a0 100644 --- a/oracles/membership-oracles/src/main/java/module-info.java +++ b/oracles/membership-oracles/src/main/java/module-info.java @@ -36,6 +36,7 @@ // 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; + requires net.automatalib.common.setting; exports de.learnlib.oracle.membership; } 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..6b3ff0d56 --- /dev/null +++ b/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/CLIOutputAdaptiveOracleTest.java @@ -0,0 +1,55 @@ +/* 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 java.util.List; + +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), + CLIOutputAdaptiveOracleTest::parseOutput, + 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)); + } + + public static Integer parseOutput(List input) { + return Integer.parseInt(input.get(0)); + } + +} 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..7b19b479f 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 @@ -17,6 +17,7 @@ import java.net.URISyntaxException; import java.util.Arrays; +import java.util.List; import net.automatalib.word.Word; import org.testng.Assert; @@ -28,14 +29,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 +48,29 @@ 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(List input, Integer offset) { + if (input.isEmpty()) { + return Word.epsilon(); + } + + if (input.get(0).isBlank()) { + throw new IllegalStateException(); + } + + return input.stream().map(Integer::parseInt).skip(offset).collect(Word.collector()); } } 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..0f7b235e5 --- /dev/null +++ b/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/StdInOutputAdaptiveOracleTest.java @@ -0,0 +1,51 @@ +/* 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 java.util.List; + +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), + CLIOutputAdaptiveOracleTest::parseOutput, + 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/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..29be60747 --- /dev/null +++ b/oracles/membership-oracles/src/test/resources/stateful_stdin_sul.py @@ -0,0 +1,29 @@ +#!/bin/python +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..1280e951f 100755 --- a/oracles/membership-oracles/src/test/resources/stateful_sul.py +++ b/oracles/membership-oracles/src/test/resources/stateful_sul.py @@ -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..10dd54763 --- /dev/null +++ b/oracles/membership-oracles/src/test/resources/stateless_stdin_sul.py @@ -0,0 +1,16 @@ +#!/bin/python +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..bc576afbb 100755 --- a/oracles/membership-oracles/src/test/resources/stateless_sul.py +++ b/oracles/membership-oracles/src/test/resources/stateless_sul.py @@ -4,16 +4,9 @@ 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) From ed50b8d2ceee70233d72721e09c621c7817151ac Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Fri, 24 Jul 2026 18:30:17 +0200 Subject: [PATCH 10/34] cleanup CLI oracles --- .../de/learnlib/cli/factory/MQOFactory.java | 10 ++-- oracles/membership-oracles/pom.xml | 4 ++ .../learnlib/oracle/membership/CLIOracle.java | 4 +- .../membership/CLIOutputAdaptiveOracle.java | 33 +++++------ .../oracle/membership/CLIOutputOracle.java | 43 +++++++------- .../oracle/membership/StdInOracle.java | 12 ++-- .../membership/StdInOutputAdaptiveOracle.java | 36 +++++------- .../oracle/membership/StdInOutputOracle.java | 56 +++++++++---------- .../src/main/java/module-info.java | 2 +- .../CLIOutputAdaptiveOracleTest.java | 9 +-- .../membership/CLIOutputOracleTest.java | 12 ++-- .../StdInOutputAdaptiveOracleTest.java | 16 ++++-- 12 files changed, 118 insertions(+), 119 deletions(-) diff --git a/cli/src/main/java/de/learnlib/cli/factory/MQOFactory.java b/cli/src/main/java/de/learnlib/cli/factory/MQOFactory.java index 481563ea8..ae1053228 100644 --- a/cli/src/main/java/de/learnlib/cli/factory/MQOFactory.java +++ b/cli/src/main/java/de/learnlib/cli/factory/MQOFactory.java @@ -106,9 +106,9 @@ private static AdaptiveMembershipOracle getAdaptiveOracle(Option AdaptiveMembershipOracle oracle; if (options.stdin) { - oracle = new StdInOutputAdaptiveOracle<>(buildCommandLine(options), l -> l.get(0), options.reset); + oracle = new StdInOutputAdaptiveOracle<>(buildCommandLine(options), Function.identity(), options.reset); } else { - oracle = new CLIOutputAdaptiveOracle<>(buildCommandLine(options), l -> l.get(0), options.reset); + oracle = new CLIOutputAdaptiveOracle<>(buildCommandLine(options), Function.identity(), options.reset); } @@ -152,7 +152,7 @@ private static void verifyReset(Options options) { } } - class OutputTransformer implements BiFunction, Integer, Word> { + class OutputTransformer implements BiFunction> { private final Pattern pattern; @@ -161,12 +161,12 @@ public OutputTransformer(Options options) { } @Override - public Word apply(List string, Integer offset) { + public Word apply(String string, Integer suffixLength) { if (string.isEmpty()) { return Word.epsilon(); } final String[] orig = pattern.split(string); - return Word.fromArray(orig, offset, orig.length - offset); + return Word.fromArray(orig, orig.length - suffixLength, suffixLength); } } } 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 2d39507d5..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 @@ -31,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. * 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 index c07d46de5..9c5a50103 100644 --- 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 @@ -17,16 +17,15 @@ import java.io.IOException; import java.io.StringReader; -import java.io.StringWriter; -import java.util.ArrayList; 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 net.automatalib.word.Word; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,13 +33,11 @@ * 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 - * {@code reset} symbol has been specified, this oracle assumes either a stateless ({@code reset == null}) or stateful - * ({@code reset != null}) communication. + * 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. *

- * In a stateless communication, all symbols of a query are passed to the program at once and invocations should be - * 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 {@code outputTransformer} is used to transform responses of the individual invocations. * * @param * input symbol type @@ -52,7 +49,7 @@ public class CLIOutputAdaptiveOracle implements SingleAdaptiveMembershipOr private static final Logger LOGGER = LoggerFactory.getLogger(CLIOutputAdaptiveOracle.class); private final List commandLine; - private final Function, O> outputTransformer; + private final Function outputTransformer; private final String reset; /** @@ -61,12 +58,11 @@ public class CLIOutputAdaptiveOracle implements SingleAdaptiveMembershipOr * @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 of an individual invocation. * @param reset * the symbol passed to the program to indicate a reset */ - public CLIOutputAdaptiveOracle(List commandLine, Function, O> outputTransformer, String reset) { + public CLIOutputAdaptiveOracle(List commandLine, Function outputTransformer, String reset) { this.commandLine = commandLine; this.reset = reset; this.outputTransformer = outputTransformer; @@ -82,16 +78,17 @@ public void processQuery(AdaptiveQuery query) { Response response; do { - final List list = new ArrayList<>(); + // 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, list::add, LOGGER::warn); - logResult(list); + ProcessUtil.invokeProcess(command, sj::add, LOGGER::warn); + logResult(sj); - final O output = outputTransformer.apply(list); + final O output = outputTransformer.apply(sj.toString()); response = query.processOutput(output); if (response == Response.RESET) { @@ -107,7 +104,7 @@ private static void logInvocation(String[] command) { LOGGER.debug("Invoking '{}'", (Object) command); } - private static void logResult(List output) { + 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 47f34ca41..3193f0ca9 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 @@ -16,8 +16,6 @@ package de.learnlib.oracle.membership; import java.io.IOException; -import java.io.StringReader; -import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.StringJoiner; @@ -35,13 +33,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 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 @@ -53,7 +55,7 @@ public class CLIOutputOracle implements SingleQueryOracle { private static final Logger LOGGER = LoggerFactory.getLogger(CLIOutputOracle.class); private final List commandLine; - private final BiFunction, Integer, D> outputTransformer; + private final BiFunction outputTransformer; private final @Nullable String reset; /** @@ -63,11 +65,11 @@ public class CLIOutputOracle 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 suffix for properly offsetting potentially {@link Word}-based output types. * * @see #CLIOutputOracle(List, BiFunction, String) */ - public CLIOutputOracle(List commandLine, BiFunction, Integer, D> outputTransformer) { + public CLIOutputOracle(List commandLine, BiFunction outputTransformer) { this(commandLine, outputTransformer, null); } @@ -78,13 +80,13 @@ public CLIOutputOracle(List commandLine, BiFunction, Intege * 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 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, Integer, D> outputTransformer, - String reset) { + BiFunction outputTransformer, + @Nullable String reset) { this.commandLine = commandLine; this.reset = reset; this.outputTransformer = outputTransformer; @@ -110,13 +112,14 @@ private D answerStatelessQuery(Word prefix, Word suffix) { args[idx++] = Objects.toString(s); } - final List output = new ArrayList<>(); + // 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, output::add, LOGGER::warn); - logResult(output); - return outputTransformer.apply(output, prefix.length()); + ProcessUtil.invokeProcess(args, sj::add, LOGGER::warn); + logResult(sj); + return outputTransformer.apply(sj.toString(), suffix.length()); } catch (IOException | InterruptedException e) { throw new IllegalStateException(e); } @@ -124,7 +127,8 @@ private D answerStatelessQuery(Word prefix, Word suffix) { @RequiresNonNull("this.reset") private D answerStatefulQuery(Word prefix, Word suffix) { - final List output = new ArrayList<>(); + // ProcessUtil calls the stdout consumer for every line, so replicate the newlines in the output + final StringJoiner sj = new StringJoiner(System.lineSeparator()); try { final String[] resetCommand = CLIOracle.toCommand(commandLine, reset); @@ -132,27 +136,24 @@ private D answerStatefulQuery(Word prefix, Word suffix) { ProcessUtil.invokeProcess(resetCommand, LOGGER::debug, LOGGER::warn); for (I p : prefix) { - answerStatefulSymbol(p, output); + answerStatefulSymbol(p, sj); } for (I s : suffix) { - answerStatefulSymbol(s, output); + answerStatefulSymbol(s, sj); } - return outputTransformer.apply(output, prefix.length()); + return outputTransformer.apply(sj.toString(), suffix.length()); } catch (IOException | InterruptedException e) { throw new IllegalStateException(e); } } - private void answerStatefulSymbol(I i, List output) throws IOException, InterruptedException { - // ProcessUtil calls the stdout consumer for every line, so replicate the newlines in the output - final StringJoiner sj = new StringJoiner(System.lineSeparator()); + 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); - output.add(sj.toString()); } private static void logInvocation(String[] command) { 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 fd2048b0a..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 @@ -35,12 +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. 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. * @@ -50,7 +50,7 @@ public class StdInOracle implements SingleQueryOracle { private static final Logger LOGGER = LoggerFactory.getLogger(StdInOracle.class); - private static final String DELIM = + private static final String DELIMITER = AutomataLibSettings.getInstance().getProperty(AutomataLibProperty.WORD_SYMBOL_SEPARATOR, " "); private final List commandLine; @@ -87,7 +87,7 @@ public Boolean answerQuery(Word prefix, Word suffix) { } private boolean answerStatelessQuery(Word prefix, Word suffix) { - final StringJoiner sj = new StringJoiner(DELIM); + final StringJoiner sj = new StringJoiner(DELIMITER); for (I p : prefix) { sj.add(String.valueOf(p)); 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 index 112f8f3a7..cb7455948 100644 --- 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 @@ -17,16 +17,16 @@ import java.io.IOException; import java.io.StringReader; -import java.util.ArrayList; 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 net.automatalib.word.Word; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,15 +34,11 @@ * 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. 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. *

- * 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. + * The {@code outputTransformer} is used to transform responses of the individual invocations. * * @param * input symbol type @@ -54,7 +50,7 @@ public class StdInOutputAdaptiveOracle implements SingleAdaptiveMembership private static final Logger LOGGER = LoggerFactory.getLogger(StdInOutputAdaptiveOracle.class); private final List commandLine; - private final Function, O> outputTransformer; + private final Function outputTransformer; private final String reset; /** @@ -63,14 +59,11 @@ public class StdInOutputAdaptiveOracle implements SingleAdaptiveMembership * @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 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 of an individual invocation. * @param reset * the symbol passed to the program to indicate a reset */ - public StdInOutputAdaptiveOracle(List commandLine, - Function, O> outputTransformer, - String reset) { + public StdInOutputAdaptiveOracle(List commandLine, Function outputTransformer, String reset) { this.commandLine = commandLine; this.outputTransformer = outputTransformer; this.reset = reset; @@ -85,17 +78,18 @@ public void processQuery(AdaptiveQuery query) { Response response; do { - final List list = new ArrayList<>(); + // 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)), - list::add, + sj::add, LOGGER::warn); - logResult(list); + logResult(sj); - final O output = outputTransformer.apply(list); + final O output = outputTransformer.apply(sj.toString()); response = query.processOutput(output); if (response == Response.RESET) { @@ -111,7 +105,7 @@ private static void logInvocation(List command, Object payload) { LOGGER.debug("Invoking '{}' with payload '{}'", command, payload); } - private static void logResult(List output) { + 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 4a16a7f39..752b9b509 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 @@ -17,7 +17,6 @@ import java.io.IOException; import java.io.StringReader; -import java.util.ArrayList; import java.util.List; import java.util.StringJoiner; import java.util.function.BiFunction; @@ -36,15 +35,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 @@ -54,11 +55,11 @@ public class StdInOutputOracle implements SingleQueryOracle { private static final Logger LOGGER = LoggerFactory.getLogger(StdInOutputOracle.class); - private static final String DELIM = + private static final String DELIMITER = AutomataLibSettings.getInstance().getProperty(AutomataLibProperty.WORD_SYMBOL_SEPARATOR, " "); private final List commandLine; - private final BiFunction, Integer, D> outputTransformer; + private final BiFunction outputTransformer; private final @Nullable String reset; /** @@ -68,11 +69,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 suffix for properly offsetting potentially {@link Word}-based output types. * * @see #StdInOutputOracle(List, BiFunction, String) */ - public StdInOutputOracle(List commandLine, BiFunction, Integer, D> outputTransformer) { + public StdInOutputOracle(List commandLine, BiFunction outputTransformer) { this(commandLine, outputTransformer, null); } @@ -83,13 +84,13 @@ public StdInOutputOracle(List commandLine, BiFunction, Inte * 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 suffix for properly offsetting potentially {@link Word}-based output types. * @param reset * the symbol passed to the program to indicate a reset */ public StdInOutputOracle(List commandLine, - BiFunction, Integer, D> outputTransformer, - String reset) { + BiFunction outputTransformer, + @Nullable String reset) { this.commandLine = commandLine; this.outputTransformer = outputTransformer; this.reset = reset; @@ -101,7 +102,7 @@ public D answerQuery(Word prefix, Word suffix) { } private D answerStatelessQuery(Word prefix, Word suffix) { - final StringJoiner input = new StringJoiner(DELIM); + final StringJoiner input = new StringJoiner(DELIMITER); for (I p : prefix) { input.add(String.valueOf(p)); @@ -112,13 +113,14 @@ private D answerStatelessQuery(Word prefix, Word suffix) { } try { - final List output = new ArrayList<>(); + // 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()), output::add, LOGGER::warn); - logResult(output); + ProcessUtil.invokeProcess(commandLine, new StringReader(input.toString()), sj::add, LOGGER::warn); + logResult(sj); - return outputTransformer.apply(output, prefix.length()); + return outputTransformer.apply(sj.toString(), suffix.length()); } catch (IOException | InterruptedException e) { throw new IllegalStateException(e); } @@ -126,33 +128,31 @@ private D answerStatelessQuery(Word prefix, Word suffix) { @RequiresNonNull("this.reset") private D answerStatefulQuery(Word prefix, Word suffix) { - final List output = new ArrayList<>(); + // 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) { - answerStatefulSymbol(p, output); + answerStatefulSymbol(p, sj); } for (I s : suffix) { - answerStatefulSymbol(s, output); + answerStatefulSymbol(s, sj); } - return outputTransformer.apply(output, prefix.length()); + return outputTransformer.apply(sj.toString(), suffix.length()); } catch (IOException | InterruptedException e) { throw new IllegalStateException(e); } } - private void answerStatefulSymbol(I i, List output) throws IOException, InterruptedException { - // ProcessUtil calls the stdout consumer for every line, so replicate the newlines in the output - final StringJoiner sj = new StringJoiner(System.lineSeparator()); + 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); - output.add(sj.toString()); } private static void logInvocation(List command, Object payload) { diff --git a/oracles/membership-oracles/src/main/java/module-info.java b/oracles/membership-oracles/src/main/java/module-info.java index f673055a0..78c65c152 100644 --- a/oracles/membership-oracles/src/main/java/module-info.java +++ b/oracles/membership-oracles/src/main/java/module-info.java @@ -30,13 +30,13 @@ requires de.learnlib.api; requires net.automatalib.api; + requires net.automatalib.common.setting; requires net.automatalib.common.util; 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; - requires net.automatalib.common.setting; exports de.learnlib.oracle.membership; } 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 index 6b3ff0d56..488351911 100644 --- 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 @@ -17,7 +17,6 @@ import java.net.URISyntaxException; import java.util.Arrays; -import java.util.List; import net.automatalib.word.Word; import org.testng.Assert; @@ -30,9 +29,7 @@ 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), - CLIOutputAdaptiveOracleTest::parseOutput, - reset); + new CLIOutputAdaptiveOracle<>(Arrays.asList(PROGRAM, script), Integer::parseInt, reset); final AdaptiveTestQuery q1 = new AdaptiveTestQuery<>(Word.fromString("ab")); final AdaptiveTestQuery q2 = @@ -48,8 +45,4 @@ public void testStatefulCommunication() throws URISyntaxException { Assert.assertEquals(q2.getOutputs().get(1).toWord(), Word.fromSymbols(97, 98)); } - public static Integer parseOutput(List input) { - return Integer.parseInt(input.get(0)); - } - } 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 7b19b479f..7aa19928e 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 @@ -17,7 +17,6 @@ import java.net.URISyntaxException; import java.util.Arrays; -import java.util.List; import net.automatalib.word.Word; import org.testng.Assert; @@ -62,15 +61,18 @@ public void testStatefulCommunication() throws URISyntaxException { Assert.assertThrows(() -> brokenOracle.answerQuery(Word.fromLetter('a'), Word.fromString("ab"))); } - static Word parseOutput(List input, Integer offset) { - if (input.isEmpty()) { + static Word parseOutput(String input, Integer suffix) { + if (suffix == 0) { return Word.epsilon(); } - if (input.get(0).isBlank()) { + if (input.isBlank()) { throw new IllegalStateException(); } - return input.stream().map(Integer::parseInt).skip(offset).collect(Word.collector()); + final Word result = + Arrays.stream(input.split(System.lineSeparator())).map(Integer::parseInt).collect(Word.collector()); + + return result.subWord(result.size() - suffix); } } 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 index 0f7b235e5..6151e4e18 100644 --- 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 @@ -17,7 +17,6 @@ import java.net.URISyntaxException; import java.util.Arrays; -import java.util.List; import net.automatalib.word.Word; import org.testng.Assert; @@ -30,9 +29,7 @@ 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), - CLIOutputAdaptiveOracleTest::parseOutput, - reset); + new StdInOutputAdaptiveOracle<>(Arrays.asList(PROGRAM, script), Integer::parseInt, reset); final AdaptiveTestQuery q1 = new AdaptiveTestQuery<>(Word.fromString("ab")); final AdaptiveTestQuery q2 = @@ -46,6 +43,17 @@ public void testStatefulCommunication() throws URISyntaxException { 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)); } } From 38f87743ef0c81cc5934cb308b4fcc32da27fb9d Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Fri, 24 Jul 2026 21:23:42 +0200 Subject: [PATCH 11/34] experiment: allow for logging intermediate hypotheses --- .../cli/factory/SerializerFactory.java | 29 +++---- .../de/learnlib/cli/util/AbstractRunner.java | 13 ++-- .../java/de/learnlib/util/Experiment.java | 77 +++++++++++++++++-- 3 files changed, 85 insertions(+), 34 deletions(-) diff --git a/cli/src/main/java/de/learnlib/cli/factory/SerializerFactory.java b/cli/src/main/java/de/learnlib/cli/factory/SerializerFactory.java index 82e472ca7..efed280fb 100644 --- a/cli/src/main/java/de/learnlib/cli/factory/SerializerFactory.java +++ b/cli/src/main/java/de/learnlib/cli/factory/SerializerFactory.java @@ -20,7 +20,6 @@ import de.learnlib.cli.option.Options; import de.learnlib.cli.option.Output; -import net.automatalib.automaton.Automaton; import net.automatalib.automaton.fsa.DFA; import net.automatalib.automaton.fsa.NFA; import net.automatalib.automaton.procedural.SBA; @@ -28,7 +27,6 @@ import net.automatalib.automaton.procedural.SPMM; import net.automatalib.automaton.transducer.MealyMachine; import net.automatalib.automaton.vpa.OneSEVPA; -import net.automatalib.graph.concept.GraphViewable; import net.automatalib.serialization.InputModelSerializer; import net.automatalib.serialization.aut.AUTWriter; import net.automatalib.serialization.ba.BAWriter; @@ -53,7 +51,7 @@ public interface SerializerFactory> private static InputModelSerializer> getDFASerializer(Options options) { final InputModelSerializer> dfaSerializer = - SerializerFactory.getDFASerializerInternal(options); + SerializerFactory.getDFASerializerInternal(options); return dfaSerializer::writeModel; } @@ -61,7 +59,7 @@ private static InputModelSerializer> getDFASerializer(Options o return switch (options.format) { case AUT -> new AUTWriter<>(); case BA -> new BAWriter<>(); - case DOT -> dotAutomatonWriter(); + case DOT -> DOTSerializationProvider.forAutomaton(); case LEARNLIBV2 -> LearnLibV2Serialization.getInstance(); case MATA -> new MataNFAWriter<>(); case SAF -> SAFWriters.dfa(); @@ -71,7 +69,7 @@ private static InputModelSerializer> getDFASerializer(Options o private static InputModelSerializer> getMealySerializer(Options options) { return switch (options.format) { - case DOT -> dotAutomatonWriter(); + case DOT -> DOTSerializationProvider.forAutomaton(); case SAF -> SAFWriters.mealy(DataOutput::writeUTF); case TAF -> TAFWriters.mealy(); default -> throw new UnsupportedCombinationException(options); @@ -80,7 +78,7 @@ private static InputModelSerializer> getDFASerializer(Options o private static InputModelSerializer> getNFASerializer(Options options) { final InputModelSerializer> nfaSerializer = - SerializerFactory.getNFASerializerInternal(options); + SerializerFactory.getNFASerializerInternal(options); return nfaSerializer::writeModel; } @@ -88,7 +86,7 @@ private static InputModelSerializer> getNFASerializer(Options o return switch (options.format) { case AUT -> new AUTWriter<>(); case BA -> new BAWriter<>(); - case DOT -> dotAutomatonWriter(); + case DOT -> DOTSerializationProvider.forAutomaton(); case MATA -> new MataNFAWriter<>(); case SAF -> SAFWriters.nfa(); default -> throw new UnsupportedCombinationException(options); @@ -97,41 +95,32 @@ private static InputModelSerializer> getNFASerializer(Options o private static InputModelSerializer> getSBASerializer(Options options) { if (options.format == Output.DOT) { - return dotGraphWriter(); + return DOTSerializationProvider.forGraphViewableInput(); } throw new UnsupportedCombinationException(options); } private static InputModelSerializer> getSPASerializer(Options options) { if (options.format == Output.DOT) { - return dotGraphWriter(); + return DOTSerializationProvider.forGraphViewableInput(); } throw new UnsupportedCombinationException(options); } private static InputModelSerializer> getSPMMSerializer(Options options) { if (options.format == Output.DOT) { - return dotGraphWriter(); + return DOTSerializationProvider.forGraphViewableInput(); } throw new UnsupportedCombinationException(options); } private static InputModelSerializer> getVPASerializer(Options options) { if (options.format == Output.DOT) { - return dotGraphWriter(); + return DOTSerializationProvider.forGraphViewableInput(); } throw new UnsupportedCombinationException(options); } - private static > InputModelSerializer dotAutomatonWriter() { - return (os, model, inputs) -> DOTSerializationProvider.getInstance() - .writeModel(os, model.transitionGraphView(inputs)); - } - - private static & GraphViewable> InputModelSerializer dotGraphWriter() { - return (os, model, inputs) -> DOTSerializationProvider.getInstance().writeModel(os, model.graphView()); - } - class UnsupportedCombinationException extends IllegalArgumentException { UnsupportedCombinationException(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 index 43b7a961e..133834257 100644 --- a/cli/src/main/java/de/learnlib/cli/util/AbstractRunner.java +++ b/cli/src/main/java/de/learnlib/cli/util/AbstractRunner.java @@ -24,6 +24,7 @@ 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; @@ -35,7 +36,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public abstract class AbstractRunner, M extends SimpleTS & FiniteRepresentation, I, D, OR> implements Runner { +public abstract class AbstractRunner, M extends SimpleTS & FiniteRepresentation, I, D, OR> + implements Runner { private static final Logger LOGGER = LoggerFactory.getLogger(AbstractRunner.class); @@ -58,7 +60,6 @@ public AbstractRunner(Function alphabetCreator, } @Override - @SuppressWarnings("PMD.AvoidThrowingRawExceptionTypes") public void run(Options options) { final A alphabet = alphabetCreator.apply(options); @@ -84,14 +85,14 @@ public void run(Options options) { final EquivalenceOracle eqo = eqoCreator.apply(options, eqoOracle); final InputModelSerializer serializer = serializerCreator.apply(options); - final Experiment experiment = new Experiment<>(learner, eqo, alphabet); + final Experiment experiment = new Experiment<>(learner, eqo, alphabet, serializer); experiment.run(); final M hyp = experiment.getFinalHypothesis(); if (options.statistics) { final StatisticsService service = Statistics.getService(); - LOGGER.info(service.print()); + LOGGER.info(Category.STATISTIC, service.print()); service.clear(); } @@ -99,12 +100,12 @@ public void run(Options options) { final ByteArrayOutputStream baos = new ByteArrayOutputStream(); serializer.writeModel(baos, hyp, alphabet); - LOGGER.info("Hypothesis:\n{}", baos.toString(StandardCharsets.UTF_8)); + 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) { - throw new RuntimeException(e); + LOGGER.warn("Could not write hypothesis", e); } } 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..559a2b8e7 100644 --- a/commons/util/src/main/java/de/learnlib/util/Experiment.java +++ b/commons/util/src/main/java/de/learnlib/util/Experiment.java @@ -15,7 +15,9 @@ */ 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.logging.Category; @@ -24,10 +26,12 @@ import de.learnlib.statistic.Statistics; import de.learnlib.statistic.StatisticsKey; import de.learnlib.statistic.StatisticsService; +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; @@ -68,10 +72,52 @@ public class Experiment { private final ExperimentImpl impl; private @Nullable A finalHypothesis; + /** + * Constructor. Delegates to + * {@link Experiment#Experiment(LearningAlgorithm, EquivalenceOracle, Alphabet, InputModelSerializer)} using + * {@code null} for {@code serializer}. + * + * @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 + * input symbol type + * @param + * output domain type + * + * @see Experiment#Experiment(LearningAlgorithm, EquivalenceOracle, Alphabet, InputModelSerializer) + */ public Experiment(LearningAlgorithm learningAlgorithm, EquivalenceOracle equivalenceAlgorithm, - Collection inputs) { - this.impl = new ExperimentImpl<>(learningAlgorithm, equivalenceAlgorithm, inputs); + Alphabet inputs) { + this(learningAlgorithm, equivalenceAlgorithm, inputs, null); + } + + /** + * 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) + * @param + * input symbol type + * @param + * output domain type + */ + public Experiment(LearningAlgorithm learningAlgorithm, + EquivalenceOracle equivalenceAlgorithm, + Alphabet inputs, + @Nullable InputModelSerializer serializer) { + this.impl = new ExperimentImpl<>(learningAlgorithm, equivalenceAlgorithm, inputs, serializer); } /** @@ -111,16 +157,19 @@ private final class ExperimentImpl { private final LearningAlgorithm learningAlgorithm; private final EquivalenceOracle equivalenceAlgorithm; - private final Collection inputs; + private final Alphabet inputs; + private final @Nullable InputModelSerializer serializer; private final StatisticsService statistics; private int rounds; ExperimentImpl(LearningAlgorithm learningAlgorithm, EquivalenceOracle equivalenceAlgorithm, - Collection inputs) { + Alphabet inputs, + @Nullable InputModelSerializer serializer) { this.learningAlgorithm = learningAlgorithm; this.equivalenceAlgorithm = equivalenceAlgorithm; this.inputs = inputs; + this.serializer = serializer; this.statistics = Statistics.getService(); } @@ -137,6 +186,18 @@ A run() { while (true) { final A hyp = learningAlgorithm.getHypothesisModel(); + 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.PHASE, "Searching for counterexample"); statistics.startOrResumeClock(KEY_DUR_CEX, Experiment.this); @@ -169,7 +230,7 @@ public static class DFAExperiment extends Experiment> { public DFAExperiment(LearningAlgorithm, I, Boolean> learningAlgorithm, EquivalenceOracle, I, Boolean> equivalenceAlgorithm, - Collection inputs) { + Alphabet inputs) { super(learningAlgorithm, equivalenceAlgorithm, inputs); } @@ -179,7 +240,7 @@ public static class MealyExperiment extends Experiment, I, Word> learningAlgorithm, EquivalenceOracle, I, Word> equivalenceAlgorithm, - Collection inputs) { + Alphabet inputs) { super(learningAlgorithm, equivalenceAlgorithm, inputs); } @@ -189,7 +250,7 @@ public static class MooreExperiment extends Experiment, I, Word> learningAlgorithm, EquivalenceOracle, I, Word> equivalenceAlgorithm, - Collection inputs) { + Alphabet inputs) { super(learningAlgorithm, equivalenceAlgorithm, inputs); } From 0530809b039fd645457d6304c22b75a38c81885f Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Fri, 24 Jul 2026 21:24:05 +0200 Subject: [PATCH 12/34] jacoco: also depend on cli module before aggregating reports --- distribution/pom.xml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/distribution/pom.xml b/distribution/pom.xml index df06ae263..723340b3c 100644 --- a/distribution/pom.xml +++ b/distribution/pom.xml @@ -270,13 +270,19 @@ limitations under the License. - + de.learnlib learnlib-examples ${project.version} test + + de.learnlib + learnlib-cli + ${project.version} + test + From 2183cb3aaabb574227dce57743c984d020bf07ec Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Fri, 24 Jul 2026 21:58:16 +0200 Subject: [PATCH 13/34] cli: improve documentation --- .../java/de/learnlib/cli/Application.java | 2 +- .../de/learnlib/cli/option/EQOParams.java | 76 +++++++++---------- cli/src/main/resources/application.properties | 68 +++++++++-------- 3 files changed, 77 insertions(+), 69 deletions(-) diff --git a/cli/src/main/java/de/learnlib/cli/Application.java b/cli/src/main/java/de/learnlib/cli/Application.java index 83faaf4d4..09facb9e3 100644 --- a/cli/src/main/java/de/learnlib/cli/Application.java +++ b/cli/src/main/java/de/learnlib/cli/Application.java @@ -33,7 +33,7 @@ descriptionHeading = "%nDescription:%n%n", parameterListHeading = "%nParameters:%n", optionListHeading = "%nOptions:%n", - description = "Runs an active automata learning process by invoking the provided SUL to answer membership queries. You may use the documentation available at https://learnlib.de/learnlib/maven-site/ to obtain additional information on certain components of LearnLib.") + description = "Runs an active automata learning process by invoking the provided SUL 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"; diff --git a/cli/src/main/java/de/learnlib/cli/option/EQOParams.java b/cli/src/main/java/de/learnlib/cli/option/EQOParams.java index d65f7542c..3cf2986a0 100644 --- a/cli/src/main/java/de/learnlib/cli/option/EQOParams.java +++ b/cli/src/main/java/de/learnlib/cli/option/EQOParams.java @@ -50,16 +50,16 @@ public static class KWayStateMethod { descriptionKey = "param.eqo.kway-s.k") public int k; - @Option(names = "--eqo-kway-s-len", + @Option(names = "--eqo-kway-s-randomWalkLen", defaultValue = "20", paramLabel = "", - descriptionKey = "param.eqo.kway-s.len") + descriptionKey = "param.eqo.kway-s.randomWalkLen") public int randomWalkLen; - @Option(names = "--eqo-kway-s-method", + @Option(names = "--eqo-kway-s-combinationMethod", defaultValue = "PERMUTATIONS", paramLabel = "", - descriptionKey = "param.eqo.kway-s.method") + descriptionKey = "param.eqo.kway-s.combinationMethod") public CombinationMethod combinationMethod; @Option(names = "--eqo-kway-s-seed", @@ -77,40 +77,40 @@ public static class KWayTransitionMethod { descriptionKey = "param.eqo.kway-t.k") public int k; - @Option(names = "--eqo-kway-t-len", + @Option(names = "--eqo-kway-t-randomWalkLen", defaultValue = "10", paramLabel = "", - descriptionKey = "param.eqo.kway-t.len") + descriptionKey = "param.eqo.kway-t.randomWalkLen") public int randomWalkLen; - @Option(names = "--eqo-kway-t-num", - defaultValue = "1000", + @Option(names = "--eqo-kway-t-numGeneratePaths", + defaultValue = "100", paramLabel = "", - descriptionKey = "param.eqo.kway-t.num") + descriptionKey = "param.eqo.kway-t.numGeneratePaths") public int numGeneratePaths; - @Option(names = "--eqo-kway-t-path", + @Option(names = "--eqo-kway-t-maxPathLen", defaultValue = "50", paramLabel = "", - descriptionKey = "param.eqo.kway-t.path") + descriptionKey = "param.eqo.kway-t.maxPathLen") public int maxPathLen; - @Option(names = "--eqo-kway-t-step", + @Option(names = "--eqo-kway-t-maxNumberOfSteps", defaultValue = "100", paramLabel = "", - descriptionKey = "param.eqo.kway-t.step") + descriptionKey = "param.eqo.kway-t.maxNumberOfSteps") public int maxNumberOfSteps; - @Option(names = "--eqo-kway-t-metric", + @Option(names = "--eqo-kway-t-optimizationMetric", defaultValue = "STEPS", paramLabel = "", - descriptionKey = "param.eqo.kway-t.metric") + descriptionKey = "param.eqo.kway-t.optimizationMetric") public OptimizationMetric optimizationMetric; - @Option(names = "--eqo-kway-t-method", + @Option(names = "--eqo-kway-t-generationMethod", defaultValue = "RANDOM", paramLabel = "", - descriptionKey = "param.eqo.kway-t.method") + descriptionKey = "param.eqo.kway-t.generationMethod") public GenerationMethod generationMethod; @Option(names = "--eqo-kway-t-seed", @@ -122,22 +122,22 @@ public static class KWayTransitionMethod { public static class RandomMethod { - @Option(names = "--eqo-random-min", + @Option(names = "--eqo-random-minLength", defaultValue = "10", paramLabel = "", - descriptionKey = "param.eqo.random.min") + descriptionKey = "param.eqo.random.minLength") public int minLength; - @Option(names = "--eqo-random-max", + @Option(names = "--eqo-random-maxLength", defaultValue = "20", paramLabel = "", - descriptionKey = "param.eqo.random.max") + descriptionKey = "param.eqo.random.maxLength") public int maxLength; - @Option(names = "--eqo-random-num", - defaultValue = "1000", + @Option(names = "--eqo-random-maxTests", + defaultValue = "100", paramLabel = "", - descriptionKey = "param.eqo.random.num") + descriptionKey = "param.eqo.random.maxTests") public int maxTests; @Option(names = "--eqo-random-seed", @@ -149,20 +149,20 @@ public static class RandomMethod { public static class RandomWMethod { - @Option(names = "--eqo-random-w-min", + @Option(names = "--eqo-random-w-minimalSize", defaultValue = "0", paramLabel = "", - descriptionKey = "param.eqo.random-w.min") + descriptionKey = "param.eqo.random-w.minimalSize") public int minimalSize; - @Option(names = "--eqo-random-w-len", + @Option(names = "--eqo-random-w-rndLength", defaultValue = "5", paramLabel = "", - descriptionKey = "param.eqo.random-w.len") + descriptionKey = "param.eqo.random-w.rndLength") public int rndLength; @Option(names = "--eqo-random-w-bound", - defaultValue = "1000", + defaultValue = "100", paramLabel = "", descriptionKey = "param.eqo.random-w.bound") public int bound; @@ -176,20 +176,20 @@ public static class RandomWMethod { public static class RandomWpMethod { - @Option(names = "--eqo-random-wp-min", + @Option(names = "--eqo-random-wp-minimalSize", defaultValue = "0", paramLabel = "", - descriptionKey = "param.eqo.random-wp.min") + descriptionKey = "param.eqo.random-wp.minimalSize") public int minimalSize; - @Option(names = "--eqo-random-wp-len", + @Option(names = "--eqo-random-wp-rndLength", defaultValue = "5", paramLabel = "", - descriptionKey = "param.eqo.random-wp.len") + descriptionKey = "param.eqo.random-wp.rndLength") public int rndLength; @Option(names = "--eqo-random-wp-bound", - defaultValue = "1000", + defaultValue = "100", paramLabel = "", descriptionKey = "param.eqo.random-wp.bound") public int bound; @@ -221,10 +221,10 @@ public static class WMethod { descriptionKey = "param.eqo.w.lookahead") public int lookahead; - @Option(names = "--eqo-w-expected", + @Option(names = "--eqo-w-expectedSize", defaultValue = "5", paramLabel = "", - descriptionKey = "param.eqo.w.expected") + descriptionKey = "param.eqo.w.expectedSize") public int expectedSize; } @@ -236,10 +236,10 @@ public static class WpMethod { descriptionKey = "param.eqo.wp.lookahead") public int lookahead; - @Option(names = "--eqo-wp-expected", + @Option(names = "--eqo-wp-expectedSize", defaultValue = "5", paramLabel = "", - descriptionKey = "param.eqo.wp.expected") + descriptionKey = "param.eqo.wp.expectedSize") public int expectedSize; } diff --git a/cli/src/main/resources/application.properties b/cli/src/main/resources/application.properties index dc8614121..e13eba712 100644 --- a/cli/src/main/resources/application.properties +++ b/cli/src/main/resources/application.properties @@ -21,45 +21,53 @@ option.delim=The string used to split responses of the SUL into individual outpu 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. If omitted, the final hypothesis model will be printed to standard out. +option.output=The output file to write the final hypothesis model to. option.reset=Specifies the reset symbol. If provided, the SUL 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.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 binary. +option.stdin=Apply query symbols via standard-in as opposed to argument lists to the SUL. option.sul=The path to the SUL used for answering queries. 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.k=The 'k' parameter of the kWay state cover oracle. -param.eqo.kway-s.len=The 'randomWalkLen' parameter of the kWay state cover oracle. -param.eqo.kway-s.method=The 'combinationMethod' parameter of the kWay state cover oracle. Possible values are: ${COMPLETION-CANDIDATES}. -param.eqo.kway-s.seed=The seed of the 'random' parameter of the kWay state cover oracle. -param.eqo.kway-t.k=The 'k' parameter of the kWay transition cover oracle. -param.eqo.kway-t.len=The 'randomWalkLen' parameter of the kWay transition cover oracle. -param.eqo.kway-t.method=The 'generationMethod' parameter of the kWay transition cover oracle. Possible values are: ${COMPLETION-CANDIDATES}. -param.eqo.kway-t.metric=The 'optimizationMetric' parameter of the kWay transition cover oracle. Possible values are: ${COMPLETION-CANDIDATES}. -param.eqo.kway-t.num=The 'numGeneratePaths' parameter of the kWay transition cover oracle. -param.eqo.kway-t.path=The 'maxPathLen' parameter of the kWay transition cover oracle. -param.eqo.kway-t.seed=The seed of the 'random' parameter of the kWay transition cover oracle. -param.eqo.kway-t.step=The 'maxNumberOfSteps' parameter of the kWay transition cover oracle. -param.eqo.random-w.bound=The 'bound' parameter of the random W method oracle. -param.eqo.random-w.len=The 'rndLength' parameter of the random W method oracle. -param.eqo.random-w.min=The 'minimalSize' parameter of the random W method oracle. -param.eqo.random-w.seed=The seed of the 'random' parameter of the random W method oracle. -param.eqo.random-wp.bound=The 'bound' parameter of the random Wp method oracle. -param.eqo.random-wp.len=The 'rndLength' parameter of the random Wp method oracle. -param.eqo.random-wp.min=The 'minimalSize' parameter of the random Wp method oracle. -param.eqo.random-wp.seed=The seed of the 'random' parameter of the random Wp method oracle. -param.eqo.random.max=The 'maxLength' parameter of the random word oracle. -param.eqo.random.min=The 'minLength' parameter of the random word oracle. -param.eqo.random.num=The 'maxTests' parameter of the random word oracle. -param.eqo.random.seed=The seed of the 'random' parameter of the random W method oracle. + +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.combinationMethod=The 'combinationMethod' parameter of the 'KWayStateCoverEQOracle'. Possible values are: ${COMPLETION-CANDIDATES}. +param.eqo.kway-s.seed=The seed of the 'random' parameter of the 'KWayStateCoverEQOracle'. + +param.eqo.kway-t.k=The 'k' parameter of the 'KWayTransitionCoverEQOracle'. +param.eqo.kway-t.randomWalkLen=The 'randomWalkLen' parameter of the 'KWayTransitionCoverEQOracle'. +param.eqo.kway-t.generationMethod=The 'generationMethod' parameter of the 'KWayTransitionCoverEQOracle'. Possible values are: ${COMPLETION-CANDIDATES}. +param.eqo.kway-t.optimizationMetric=The 'optimizationMetric' parameter of the 'KWayTransitionCoverEQOracle'. Possible values are: ${COMPLETION-CANDIDATES}. +param.eqo.kway-t.numGeneratePaths=The 'numGeneratePaths' parameter of the 'KWayTransitionCoverEQOracle'. +param.eqo.kway-t.maxPathLen=The 'maxPathLen' parameter of the 'KWayTransitionCoverEQOracle'. +param.eqo.kway-t.seed=The seed of the 'random' parameter of the 'KWayTransitionCoverEQOracle'. +param.eqo.kway-t.maxNumberOfSteps=The 'maxNumberOfSteps' parameter of the 'KWayTransitionCoverEQOracle'. + +param.eqo.random-w.bound=The 'bound' parameter of the 'RandomWMethodEQOracle'. +param.eqo.random-w.rndLength=The 'rndLength' parameter of the 'RandomWMethodEQOracle'. +param.eqo.random-w.minimalSize=The 'minimalSize' 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.rndLength=The 'rndLength' parameter of the 'RandomWpMethodEQOracle'. +param.eqo.random-wp.minimalSize=The 'minimalSize' 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.minLength=The 'minLength' parameter of the 'RandomWordsEQOracle'. +param.eqo.random.maxTests=The 'maxTests' 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 to split and individual symbols of a trace. -param.eqo.w.expected=The 'expectedSize' parameter for the W method oracle. -param.eqo.w.lookahead=The 'lookahead' parameter for the W method oracle. -param.eqo.wp.expected=The 'expectedSize' parameter for the Wp method oracle. -param.eqo.wp.lookahead=The 'lookahead' parameter for the Wp method oracle. + +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 From e40b54ac4ee193485693ef18e5ab3f15f1ffe286 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Sat, 25 Jul 2026 00:49:25 +0200 Subject: [PATCH 14/34] fix code-analysis --- cli/src/main/resources/application.properties | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cli/src/main/resources/application.properties b/cli/src/main/resources/application.properties index e13eba712..cf905d509 100644 --- a/cli/src/main/resources/application.properties +++ b/cli/src/main/resources/application.properties @@ -31,33 +31,33 @@ option.verbose=Increase verbosity. Each repetition increases the log level from 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.combinationMethod=The 'combinationMethod' parameter of the 'KWayStateCoverEQOracle'. Possible values are: ${COMPLETION-CANDIDATES}. param.eqo.kway-s.seed=The seed of the 'random' parameter of the 'KWayStateCoverEQOracle'. -param.eqo.kway-t.k=The 'k' parameter of the 'KWayTransitionCoverEQOracle'. -param.eqo.kway-t.randomWalkLen=The 'randomWalkLen' parameter of the 'KWayTransitionCoverEQOracle'. param.eqo.kway-t.generationMethod=The 'generationMethod' parameter of the 'KWayTransitionCoverEQOracle'. Possible values are: ${COMPLETION-CANDIDATES}. -param.eqo.kway-t.optimizationMetric=The 'optimizationMetric' parameter of the 'KWayTransitionCoverEQOracle'. Possible values are: ${COMPLETION-CANDIDATES}. -param.eqo.kway-t.numGeneratePaths=The 'numGeneratePaths' parameter of the 'KWayTransitionCoverEQOracle'. +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.kway-t.maxNumberOfSteps=The 'maxNumberOfSteps' parameter of the 'KWayTransitionCoverEQOracle'. param.eqo.random-w.bound=The 'bound' parameter of the 'RandomWMethodEQOracle'. -param.eqo.random-w.rndLength=The 'rndLength' 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.rndLength=The 'rndLength' 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.minLength=The 'minLength' 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. From 273273e09395598b00eb86efc40ad55276b62f61 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Sat, 25 Jul 2026 00:43:13 +0200 Subject: [PATCH 15/34] cli: add support for parallel oracles --- cli/pom.xml | 4 + .../de/learnlib/cli/factory/EQOFactory.java | 27 +++++-- .../de/learnlib/cli/factory/MQOFactory.java | 79 +++++++++++++------ .../java/de/learnlib/cli/option/Options.java | 4 +- cli/src/main/java/module-info.java | 1 + cli/src/main/resources/application.properties | 10 +-- 6 files changed, 89 insertions(+), 36 deletions(-) diff --git a/cli/pom.xml b/cli/pom.xml index f25c3ba1d..ab6bae24a 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -49,6 +49,10 @@ limitations under the License. de.learnlib learnlib-membership-oracles + + de.learnlib + learnlib-parallelism + de.learnlib learnlib-util diff --git a/cli/src/main/java/de/learnlib/cli/factory/EQOFactory.java b/cli/src/main/java/de/learnlib/cli/factory/EQOFactory.java index 03f58daf7..a1cc7a783 100644 --- a/cli/src/main/java/de/learnlib/cli/factory/EQOFactory.java +++ b/cli/src/main/java/de/learnlib/cli/factory/EQOFactory.java @@ -52,6 +52,8 @@ @FunctionalInterface public interface EQOFactory extends BiFunction> { + int BATCH_SIZE = 10; + EQOFactory, String, Boolean, MembershipOracle> DFA_ORACLES = EQOFactory::getRegularOracles; EQOFactory, String, Word, MembershipOracle>> @@ -78,33 +80,36 @@ public interface EQOFactory extends BiFunction eqo = switch (e) { case W -> new WMethodEQOracle<>(mqo, options.eqoParams.wMethod.lookahead, - options.eqoParams.wMethod.expectedSize); + options.eqoParams.wMethod.expectedSize, + computeBatchSize(options)); case WP -> new WpMethodEQOracle<>(mqo, options.eqoParams.wpMethod.lookahead, - options.eqoParams.wpMethod.expectedSize); + 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)); + 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), - 1); + 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), - 1); + 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, - 1); + computeBatchSize(options)); case KWAY_T -> new KWayTransitionCoverEQOracle<>(mqo, new Random(options.eqoParams.kWayTransition.seed), options.eqoParams.kWayTransition.randomWalkLen, @@ -114,7 +119,7 @@ public interface EQOFactory extends BiFunction buildSampleSetOracle(options, mqo); }; chain.addOracle(eqo); @@ -242,6 +247,14 @@ private static SampleSetEQOracle buildSampleSetOracle(Options opt return new SampleSetEQOracle().addAll(oracle, tmp); } + private static int computeBatchSize(Options options) { + if (options.sul.size() == 1) { + return 1; + } else { + return options.sul.size() * BATCH_SIZE; + } + } + class UnsupportedCombinationException extends IllegalArgumentException { UnsupportedCombinationException(Options options, EQOracle eqo) { diff --git a/cli/src/main/java/de/learnlib/cli/factory/MQOFactory.java b/cli/src/main/java/de/learnlib/cli/factory/MQOFactory.java index ae1053228..d3b5f9402 100644 --- a/cli/src/main/java/de/learnlib/cli/factory/MQOFactory.java +++ b/cli/src/main/java/de/learnlib/cli/factory/MQOFactory.java @@ -15,6 +15,7 @@ */ package de.learnlib.cli.factory; +import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -35,6 +36,7 @@ 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; @@ -46,15 +48,16 @@ public interface MQOFactory extends BiFunction { MQOFactory, Alphabet> ADAPTIVE = MQOFactory::getAdaptiveOracle; private static MembershipOracle getAcceptorOracle(Options options, Alphabet alphabet) { - verifyPath(options); - MembershipOracle oracle; - if (options.stdin) { - oracle = new StdInOracle<>(buildCommandLine(options), options.reset); + if (options.sul.size() == 1) { + oracle = buildSingleAcceptorOracle(options, options.sul.get(0)); } else { - oracle = new CLIOracle<>(buildCommandLine(options), options.reset); - + 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) { @@ -71,17 +74,26 @@ private static MembershipOracle getAcceptorOracle(Options optio return oracle; } + private 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); + } + } + private static MembershipOracle> getTransducerOracle(Options options, Alphabet alphabet) { - verifyPath(options); - MembershipOracle> oracle; - if (options.stdin) { - oracle = new StdInOutputOracle<>(buildCommandLine(options), new OutputTransformer(options), options.reset); + if (options.sul.size() == 1) { + oracle = buildSingleTransducerOracle(options, options.sul.get(0)); } else { - oracle = new CLIOutputOracle<>(buildCommandLine(options), new OutputTransformer(options), options.reset); - + 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) { @@ -98,18 +110,32 @@ private static MembershipOracle> getTransducerOracle(Option return oracle; } + private 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); + } + } + private static AdaptiveMembershipOracle getAdaptiveOracle(Options options, Alphabet alphabet) { - verifyPath(options); verifyReset(options); AdaptiveMembershipOracle oracle; - if (options.stdin) { - oracle = new StdInOutputAdaptiveOracle<>(buildCommandLine(options), Function.identity(), options.reset); + if (options.sul.size() == 1) { + oracle = buildSingleAdaptiveOracle(options, options.sul.get(0)); } else { - oracle = new CLIOutputAdaptiveOracle<>(buildCommandLine(options), Function.identity(), options.reset); - + 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) { @@ -126,8 +152,17 @@ private static AdaptiveMembershipOracle getAdaptiveOracle(Option return oracle; } - private static List buildCommandLine(Options options) { - final String absolutePath = options.sul.getAbsolutePath(); + private 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 { @@ -138,9 +173,9 @@ private static List buildCommandLine(Options options) { } } - private static void verifyPath(Options options) { - if (!options.sul.exists()) { - throw new IllegalArgumentException(String.format("Specified SUL '%s' does not exist", options.sul)); + private static void verifyPath(File file) { + if (!file.exists()) { + throw new IllegalArgumentException(String.format("Specified SUL '%s' does not exist", file)); } } diff --git a/cli/src/main/java/de/learnlib/cli/option/Options.java b/cli/src/main/java/de/learnlib/cli/option/Options.java index 371beda93..70199ad2b 100644 --- a/cli/src/main/java/de/learnlib/cli/option/Options.java +++ b/cli/src/main/java/de/learnlib/cli/option/Options.java @@ -25,8 +25,8 @@ public class Options { - @Parameters(paramLabel = "", descriptionKey = "option.sul") - public File sul; + @Parameters(paramLabel = "", descriptionKey = "option.sul", arity = "1..*") + public List sul; @Option(names = {"-t", "--type"}, defaultValue = "DFA", descriptionKey = "option.type") public Type type; diff --git a/cli/src/main/java/module-info.java b/cli/src/main/java/module-info.java index 48eb13290..129bcada3 100644 --- a/cli/src/main/java/module-info.java +++ b/cli/src/main/java/module-info.java @@ -35,6 +35,7 @@ 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; diff --git a/cli/src/main/resources/application.properties b/cli/src/main/resources/application.properties index cf905d509..c3248b3ce 100644 --- a/cli/src/main/resources/application.properties +++ b/cli/src/main/resources/application.properties @@ -15,17 +15,17 @@ app.version=${project.version} -option.args=Additional arguments to pass to the SUL. +option.args=Additional arguments to pass to the SUL(s). option.cache=Use a query cache. -option.delim=The string used to split responses of the SUL into individual output symbols. +option.delim=The string used to split responses of the SUL(s) into individual output symbols. 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 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.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.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. -option.sul=The path to the SUL used for answering queries. +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. From 480d1bbdcd531cec3cdd21cbecf1302bf1c9c5c4 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Sun, 26 Jul 2026 13:52:03 +0200 Subject: [PATCH 16/34] cli: add unit- and integration-tests --- cli/pom.xml | 9 + .../java/de/learnlib/cli/Application.java | 11 +- .../learnlib/cli/factory/AlphabetFactory.java | 16 +- .../de/learnlib/cli/factory/EQOFactory.java | 101 ++- .../learnlib/cli/factory/LearnerFactory.java | 45 +- .../de/learnlib/cli/factory/MQOFactory.java | 68 +- .../cli/factory/SerializerFactory.java | 32 +- .../de/learnlib/cli/option/EQOParams.java | 2 +- .../java/de/learnlib/cli/option/Options.java | 2 +- .../java/de/learnlib/cli/option/Type.java | 80 +-- .../de/learnlib/cli/util/AbstractRunner.java | 8 +- cli/src/main/resources/application.properties | 4 +- .../java/de/learnlib/cli/ApplicationIT.java | 175 +++-- .../cli/factory/AlphabetFactoryTest.java | 99 +++ .../learnlib/cli/factory/EQOFactoryTest.java | 626 ++++++++++++++++++ .../cli/factory/LearnerFactoryTest.java | 489 ++++++++++++++ .../learnlib/cli/factory/MQOFactoryTest.java | 329 +++++++++ .../cli/factory/SerializationFactoryTest.java | 319 +++++++++ .../learnlib/cli/util/AcceptorNullOracle.java | 27 + .../learnlib/cli/util/AdaptiveNullOracle.java | 32 + .../cli/util/TransducerNullOracle.java | 28 + .../test/java/de/learnlib/cli/util/Util.java | 147 ++++ cli/src/test/resources/logback-test.xml | 28 + cli/src/test/resources/ser/dfa.aut | 5 + cli/src/test/resources/ser/dfa.ba | 6 + cli/src/test/resources/{ => ser}/dfa.dot | 4 +- cli/src/test/resources/ser/dfa.lv2 | 4 + cli/src/test/resources/ser/dfa.mata | 9 + cli/src/test/resources/ser/dfa.saf | Bin 0 -> 36 bytes cli/src/test/resources/ser/dfa.taf | 8 + cli/src/test/resources/{ => ser}/mealy.dot | 2 +- cli/src/test/resources/ser/mealy.saf | Bin 0 -> 32 bytes cli/src/test/resources/ser/mealy.taf | 6 + cli/src/test/resources/ser/nfa.aut | 5 + cli/src/test/resources/ser/nfa.ba | 6 + cli/src/test/resources/ser/nfa.dot | 13 + cli/src/test/resources/ser/nfa.mata | 9 + cli/src/test/resources/ser/nfa.saf | Bin 0 -> 56 bytes cli/src/test/resources/ser/sba.dot | 27 + cli/src/test/resources/ser/spa.dot | 15 + cli/src/test/resources/ser/spmm.dot | 22 + cli/src/test/resources/ser/vpa.dot | 21 + cli/src/test/resources/sul/sba.py | 40 ++ cli/src/test/resources/sul/spa.py | 36 + .../test/resources/{suls => sul}/stateful.py | 0 .../test/resources/{suls => sul}/stateless.py | 0 .../stateless_broken.py} | 5 +- cli/src/test/resources/suls/stateful_stdin.py | 28 - .../vpa/RandomWellMatchedWordsEQOracle.java | 8 +- .../oracle/membership/CLIOutputOracle.java | 44 +- .../oracle/membership/StdInOutputOracle.java | 44 +- .../membership/CLIOutputOracleTest.java | 8 +- 52 files changed, 2750 insertions(+), 302 deletions(-) create mode 100644 cli/src/test/java/de/learnlib/cli/factory/AlphabetFactoryTest.java create mode 100644 cli/src/test/java/de/learnlib/cli/factory/EQOFactoryTest.java create mode 100644 cli/src/test/java/de/learnlib/cli/factory/LearnerFactoryTest.java create mode 100644 cli/src/test/java/de/learnlib/cli/factory/MQOFactoryTest.java create mode 100644 cli/src/test/java/de/learnlib/cli/factory/SerializationFactoryTest.java create mode 100644 cli/src/test/java/de/learnlib/cli/util/AcceptorNullOracle.java create mode 100644 cli/src/test/java/de/learnlib/cli/util/AdaptiveNullOracle.java create mode 100644 cli/src/test/java/de/learnlib/cli/util/TransducerNullOracle.java create mode 100644 cli/src/test/java/de/learnlib/cli/util/Util.java create mode 100644 cli/src/test/resources/logback-test.xml create mode 100644 cli/src/test/resources/ser/dfa.aut create mode 100644 cli/src/test/resources/ser/dfa.ba rename cli/src/test/resources/{ => ser}/dfa.dot (71%) create mode 100644 cli/src/test/resources/ser/dfa.lv2 create mode 100644 cli/src/test/resources/ser/dfa.mata create mode 100644 cli/src/test/resources/ser/dfa.saf create mode 100644 cli/src/test/resources/ser/dfa.taf rename cli/src/test/resources/{ => ser}/mealy.dot (81%) create mode 100644 cli/src/test/resources/ser/mealy.saf create mode 100644 cli/src/test/resources/ser/mealy.taf create mode 100644 cli/src/test/resources/ser/nfa.aut create mode 100644 cli/src/test/resources/ser/nfa.ba create mode 100644 cli/src/test/resources/ser/nfa.dot create mode 100644 cli/src/test/resources/ser/nfa.mata create mode 100644 cli/src/test/resources/ser/nfa.saf create mode 100644 cli/src/test/resources/ser/sba.dot create mode 100644 cli/src/test/resources/ser/spa.dot create mode 100644 cli/src/test/resources/ser/spmm.dot create mode 100644 cli/src/test/resources/ser/vpa.dot create mode 100755 cli/src/test/resources/sul/sba.py create mode 100755 cli/src/test/resources/sul/spa.py rename cli/src/test/resources/{suls => sul}/stateful.py (100%) rename cli/src/test/resources/{suls => sul}/stateless.py (100%) rename cli/src/test/resources/{suls/stateless_stdin.py => sul/stateless_broken.py} (67%) delete mode 100755 cli/src/test/resources/suls/stateful_stdin.py diff --git a/cli/pom.xml b/cli/pom.xml index ab6bae24a..597571de7 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -180,6 +180,15 @@ limitations under the License. + + org.checkerframework + checker-qual + test + + + org.mockito + mockito-core + org.testng testng diff --git a/cli/src/main/java/de/learnlib/cli/Application.java b/cli/src/main/java/de/learnlib/cli/Application.java index 09facb9e3..8dbe3c301 100644 --- a/cli/src/main/java/de/learnlib/cli/Application.java +++ b/cli/src/main/java/de/learnlib/cli/Application.java @@ -57,11 +57,12 @@ private void setLogLevel(Options options) { if (options.verbosity != null) { final Logger root = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); - final Level level = switch (options.verbosity.length) { - case 0 -> Level.INFO; - case 1 -> Level.DEBUG; - default -> Level.TRACE; - }; + 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/factory/AlphabetFactory.java b/cli/src/main/java/de/learnlib/cli/factory/AlphabetFactory.java index 0ca9199c6..3876816d8 100644 --- a/cli/src/main/java/de/learnlib/cli/factory/AlphabetFactory.java +++ b/cli/src/main/java/de/learnlib/cli/factory/AlphabetFactory.java @@ -16,7 +16,6 @@ package de.learnlib.cli.factory; import java.util.Objects; -import java.util.function.Function; import de.learnlib.cli.option.Options; import de.learnlib.cli.option.Symbols.ContextFreeSymbols; @@ -28,19 +27,18 @@ import net.automatalib.alphabet.impl.DefaultProceduralInputAlphabet; import net.automatalib.alphabet.impl.DefaultVPAlphabet; -@FunctionalInterface -public interface AlphabetFactory> extends Function { +public final class AlphabetFactory { - AlphabetFactory> REGULAR = AlphabetFactory::getRegularAlphabet; - AlphabetFactory> PROCEDURAL = AlphabetFactory::getProceduralAlphabet; - AlphabetFactory> VPA = AlphabetFactory::getVPAAlphabet; + private AlphabetFactory() { + // prevent instantiation + } - private static Alphabet getRegularAlphabet(Options options) { + public static Alphabet getRegularAlphabet(Options options) { final RegularSymbols inputs = validateRegularSymbols(options); return Alphabets.fromList(inputs.symbols); } - private static ProceduralInputAlphabet getProceduralAlphabet(Options options) { + 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"); @@ -50,7 +48,7 @@ private static ProceduralInputAlphabet getProceduralAlphabet(Options opt inputs.returnSymbols.get(0)); } - private static VPAlphabet getVPAAlphabet(Options options) { + public static VPAlphabet getVPAlphabet(Options options) { final ContextFreeSymbols inputs = validateContextFreeSymbols(options); return new DefaultVPAlphabet<>(Alphabets.fromList(inputs.internalSymbols), Alphabets.fromList(inputs.callSymbols), diff --git a/cli/src/main/java/de/learnlib/cli/factory/EQOFactory.java b/cli/src/main/java/de/learnlib/cli/factory/EQOFactory.java index a1cc7a783..ca79845f7 100644 --- a/cli/src/main/java/de/learnlib/cli/factory/EQOFactory.java +++ b/cli/src/main/java/de/learnlib/cli/factory/EQOFactory.java @@ -18,7 +18,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Random; -import java.util.function.BiFunction; +import java.util.regex.Pattern; import de.learnlib.cli.option.EQOracle; import de.learnlib.cli.option.Options; @@ -44,34 +44,20 @@ 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.util.automaton.fsa.NFAs; import net.automatalib.word.Word; -@FunctionalInterface -public interface EQOFactory extends BiFunction> { - - int BATCH_SIZE = 10; - - EQOFactory, String, Boolean, MembershipOracle> DFA_ORACLES = - EQOFactory::getRegularOracles; - EQOFactory, String, Word, MembershipOracle>> - MEALY_ORACLES = EQOFactory::getRegularOracles; - EQOFactory, String, Word, AdaptiveMembershipOracle> - ADAPTIVE_ORACLES = EQOFactory::getAdaptiveOracles; - EQOFactory, String, Boolean, MembershipOracle> NFA_ORACLES = - EQOFactory::getNFAOracles; - EQOFactory, String, Boolean, MembershipOracle> SBA_ORACLES = - EQOFactory::getSBAOracles; - EQOFactory, String, Boolean, MembershipOracle> SPA_ORACLES = - EQOFactory::getSPAOracles; - EQOFactory, String, Word, MembershipOracle>> SPMM_ORACLES = - EQOFactory::getSPMMOracles; - EQOFactory, String, Boolean, MembershipOracle> VPA_ORACLES = - EQOFactory::getVPAOracles; - - private static & SuffixOutput, D> EQOracleChain getRegularOracles( +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<>(); @@ -128,14 +114,14 @@ public interface EQOFactory extends BiFunction & SuffixOutput>, O> EQOracleChain> getAdaptiveOracles( + public static & SuffixOutput>, O> EQOracleChain> getAdaptiveOracles( Options options, AdaptiveMembershipOracle mqo) { return getRegularOracles(options, new Adaptive2MembershipWrapper<>(mqo)); } - private static EquivalenceOracle, String, Boolean> getNFAOracles(Options options, - MembershipOracle 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<>(); @@ -148,15 +134,16 @@ private static EquivalenceOracle, String, Boolean> getNFAOracles( return result; } - private static EquivalenceOracle, String, Boolean> getSBAOracles(Options options, - MembershipOracle mqo) { + 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); + options.eqoParams.wMethod.expectedSize, + computeBatchSize(options)); case RANDOM -> buildRandomWellMatchedOracle(options, mqo); case SAMPLE -> buildSampleSetOracle(options, mqo); default -> throw new UnsupportedCombinationException(options, e); @@ -167,18 +154,20 @@ private static EquivalenceOracle, String, Boolean> getSBAOracles( return chain; } - private static EquivalenceOracle, String, Boolean> getSPAOracles(Options options, - MembershipOracle mqo) { + 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); + options.eqoParams.wMethod.expectedSize, + computeBatchSize(options)); case WP -> new de.learnlib.oracle.equivalence.spa.WpMethodEQOracle<>(mqo, options.eqoParams.wpMethod.lookahead, - options.eqoParams.wpMethod.expectedSize); + options.eqoParams.wpMethod.expectedSize, + computeBatchSize(options)); case RANDOM -> buildRandomWellMatchedOracle(options, mqo); case SAMPLE -> buildSampleSetOracle(options, mqo); default -> throw new UnsupportedCombinationException(options, e); @@ -189,15 +178,17 @@ private static EquivalenceOracle, String, Boolean> getSPAOracles( return chain; } - private static EquivalenceOracle, String, Word> getSPMMOracles(Options options, - MembershipOracle> mqo) { + 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); + options.eqoParams.wMethod.expectedSize, + computeBatchSize(options)); + case RANDOM -> buildRandomWellMatchedOracle(options, mqo); case SAMPLE -> buildSampleSetOracle(options, mqo); default -> throw new UnsupportedCombinationException(options, e); }; @@ -207,8 +198,8 @@ private static EquivalenceOracle, String, Boolean> getSPAOracles( return chain; } - private static EquivalenceOracle, String, Boolean> getVPAOracles(Options options, - MembershipOracle mqo) { + public static EQOracleChain, String, Boolean> getVPAOracles(Options options, + MembershipOracle mqo) { final EQOracleChain, String, Boolean> chain = new EQOracleChain<>(); for (EQOracle e : options.eqos) { @@ -223,28 +214,36 @@ private static EquivalenceOracle, String, Boolean> getVPAOra return chain; } - private static RandomWellMatchedWordsEQOracle buildRandomWellMatchedOracle(Options options, - MembershipOracle oracle) { - final double callProb = 0.5; + private static RandomWellMatchedWordsEQOracle buildRandomWellMatchedOracle(Options options, + MembershipOracle oracle) { return new RandomWellMatchedWordsEQOracle<>(new Random(options.eqoParams.random.seed), oracle, - callProb, + RANDOM_CALL_PROB, options.eqoParams.random.maxTests, options.eqoParams.random.minLength, - options.eqoParams.random.maxLength); + options.eqoParams.random.maxLength, + computeBatchSize(options)); } private static SampleSetEQOracle buildSampleSetOracle(Options options, MembershipOracle oracle) { final List samples = options.eqoParams.samples.samples; - final List> tmp = new ArrayList<>(samples.size()); + 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)); + } - for (String s : options.eqoParams.samples.samples) { - String[] words = s.split(options.eqoParams.samples.split); - tmp.add(Word.fromArray(words, 0, words.length)); + sampleSetOracle.addAll(oracle, tmp); } - return new SampleSetEQOracle().addAll(oracle, tmp); + return sampleSetOracle; } private static int computeBatchSize(Options options) { @@ -255,7 +254,7 @@ private static int computeBatchSize(Options options) { } } - class UnsupportedCombinationException extends IllegalArgumentException { + 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 index 7e310dafe..6be351a79 100644 --- a/cli/src/main/java/de/learnlib/cli/factory/LearnerFactory.java +++ b/cli/src/main/java/de/learnlib/cli/factory/LearnerFactory.java @@ -15,8 +15,6 @@ */ package de.learnlib.cli.factory; -import java.util.function.Function; - import de.learnlib.algorithm.adt.learner.ADTLearner; import de.learnlib.algorithm.lsharp.LSharpMealy; import de.learnlib.algorithm.nlstar.NLStarLearner; @@ -42,18 +40,13 @@ 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; 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.oracle.AdaptiveMembershipOracle; -import de.learnlib.oracle.MembershipOracle; import net.automatalib.alphabet.Alphabet; import net.automatalib.alphabet.ProceduralInputAlphabet; import net.automatalib.alphabet.VPAlphabet; -import net.automatalib.automaton.concept.FiniteRepresentation; -import net.automatalib.automaton.fsa.DFA; import net.automatalib.automaton.fsa.NFA; import net.automatalib.automaton.procedural.SBA; import net.automatalib.automaton.procedural.SPA; @@ -62,25 +55,13 @@ import net.automatalib.automaton.vpa.OneSEVPA; import net.automatalib.word.Word; -@FunctionalInterface -public interface LearnerFactory, M extends FiniteRepresentation, D, OR> - extends Function> { +public final class LearnerFactory { - LearnerFactory, DFA, Boolean, MembershipOracle> DFA_LEARNER = LearnerFactory::getDFALearner; - LearnerFactory, MealyMachine, Word, MembershipOracle>> MEALY_LEARNER = - LearnerFactory::getMealyLearner; - LearnerFactory, MealyMachine, Word, AdaptiveMembershipOracle> ADAPTIVE_LEARNER = - LearnerFactory::getAdaptiveLearner; - LearnerFactory, NFA, Boolean, MembershipOracle> NFA_LEARNER = LearnerFactory::getNFALearner; - LearnerFactory, SBA, Boolean, MembershipOracle> SBA_LEARNER = - LearnerFactory::getSBALearner; - LearnerFactory, SPA, Boolean, MembershipOracle> SPA_LEARNER = - LearnerFactory::getSPALearner; - LearnerFactory, SPMM, Word, MembershipOracle>> SPMM_LEARNER = - LearnerFactory::getSPMMLearner; - LearnerFactory, OneSEVPA, Boolean, MembershipOracle> VPA_LEARNER = LearnerFactory::getVPALearner; + private LearnerFactory() { + // prevent instantiation + } - private static DFAConstructor, I> getDFALearner(Options options) { + public static DFAConstructor, I> getDFALearner(Options options) { return switch (options.learner) { case KV -> KearnsVaziraniDFAAdapter::new; case LLAMBDA -> LLambdaDFAAdapter::new; @@ -92,7 +73,7 @@ private static DFAConstructor, I> getDFALearner(Options options) }; } - private static MealyConstructor, I, O> getMealyLearner(Options options) { + public static MealyConstructor, I, O> getMealyLearner(Options options) { return switch (options.learner) { case DHC -> MealyDHCAdapter::new; case KV -> KearnsVaziraniMealyAdapter::new; @@ -106,7 +87,7 @@ private static MealyConstructor, I, O> getMealyLearner(Option }; } - private static AdaptiveConstructor, MealyMachine, I, O> getAdaptiveLearner(Options options) { + public static AdaptiveConstructor, MealyMachine, I, O> getAdaptiveLearner(Options options) { return switch (options.learner) { case ADT -> ADTLearner::new; case LSHARP -> LSharpMealy::new; @@ -114,30 +95,30 @@ private static MealyConstructor, I, O> getMealyLearner(Option }; } - private static PresetConstructor, NFA, I, Boolean> getNFALearner(Options options) { + public static PresetConstructor, NFA, I, Boolean> getNFALearner(Options options) { if (options.learner == Learner.NLSTAR) { return NLStarLearner::new; } throw new UnsupportedCombinationException(options); } - private static PresetConstructor, SBA, I, Boolean> getSBALearner(Options 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); } - private static PresetConstructor, SPA, I, Boolean> getSPALearner(Options options) { + public static PresetConstructor, SPA, I, Boolean> getSPALearner(Options options) { final DFAConstructor, I> learner = getDFALearner(options); return (alph, mqo) -> new SPALearner<>(alph, mqo, learner::constructLearner); } - private static PresetConstructor, SPMM, I, Word> getSPMMLearner( + 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); } - private static PresetConstructor, OneSEVPA, I, Boolean> getVPALearner(Options options) { + public static PresetConstructor, OneSEVPA, I, Boolean> getVPALearner(Options options) { return switch (options.learner) { case OP -> (alphabet, mqo) -> new OPLearnerVPABuilder().withAlphabet(alphabet).withOracle(mqo).create(); case TTT -> @@ -146,7 +127,7 @@ private static PresetConstructor, OneSEVPA, I, Boolean> }; } - class UnsupportedCombinationException extends IllegalArgumentException { + 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 index d3b5f9402..dfe4395de 100644 --- a/cli/src/main/java/de/learnlib/cli/factory/MQOFactory.java +++ b/cli/src/main/java/de/learnlib/cli/factory/MQOFactory.java @@ -17,9 +17,9 @@ import java.io.File; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.function.BiFunction; import java.util.function.Function; import java.util.regex.Pattern; @@ -40,14 +40,16 @@ import net.automatalib.alphabet.Alphabet; import net.automatalib.word.Word; -@FunctionalInterface -public interface MQOFactory extends BiFunction { +public final class MQOFactory { - MQOFactory, Alphabet> ACCEPTOR = MQOFactory::getAcceptorOracle; - MQOFactory>, Alphabet> TRANSDUCER = MQOFactory::getTransducerOracle; - MQOFactory, Alphabet> ADAPTIVE = MQOFactory::getAdaptiveOracle; + static final String SUL_KEY = "sul"; + static final String CACHE_KEY = "cache"; - private static MembershipOracle getAcceptorOracle(Options options, Alphabet alphabet) { + private MQOFactory() { + // prevent instantiation + } + + public static MembershipOracle getAcceptorOracle(Options options, Alphabet alphabet) { MembershipOracle oracle; if (options.sul.size() == 1) { @@ -61,20 +63,20 @@ private static MembershipOracle getAcceptorOracle(Options optio } if (options.statistics) { - oracle = new CounterOracle<>(oracle, "sul"); + oracle = new CounterOracle<>(oracle, SUL_KEY); } if (options.cache) { oracle = DFACaches.createDAGCache(alphabet, oracle); if (options.statistics) { - oracle = new CounterOracle<>(oracle, "cache"); + oracle = new CounterOracle<>(oracle, CACHE_KEY); } } return oracle; } - private static MembershipOracle buildSingleAcceptorOracle(Options options, File path) { + static MembershipOracle buildSingleAcceptorOracle(Options options, File path) { if (options.stdin) { return new StdInOracle<>(buildCommandLine(options, path), options.reset); } else { @@ -82,8 +84,8 @@ private static MembershipOracle buildSingleAcceptorOracle(Optio } } - private static MembershipOracle> getTransducerOracle(Options options, - Alphabet alphabet) { + public static MembershipOracle> getTransducerOracle(Options options, + Alphabet alphabet) { MembershipOracle> oracle; if (options.sul.size() == 1) { @@ -97,20 +99,20 @@ private static MembershipOracle> getTransducerOracle(Option } if (options.statistics) { - oracle = new CounterOracle<>(oracle, "sul"); + oracle = new CounterOracle<>(oracle, SUL_KEY); } if (options.cache) { oracle = MealyCaches.createDAGCache(alphabet, oracle); if (options.statistics) { - oracle = new CounterOracle<>(oracle, "cache"); + oracle = new CounterOracle<>(oracle, CACHE_KEY); } } return oracle; } - private static MembershipOracle> buildSingleTransducerOracle(Options options, File path) { + static MembershipOracle> buildSingleTransducerOracle(Options options, File path) { if (options.stdin) { return new StdInOutputOracle<>(buildCommandLine(options, path), new OutputTransformer(options), @@ -122,8 +124,8 @@ private static MembershipOracle> buildSingleTransducerOracl } } - private static AdaptiveMembershipOracle getAdaptiveOracle(Options options, - Alphabet alphabet) { + public static AdaptiveMembershipOracle getAdaptiveOracle(Options options, + Alphabet alphabet) { verifyReset(options); AdaptiveMembershipOracle oracle; @@ -139,20 +141,20 @@ private static AdaptiveMembershipOracle getAdaptiveOracle(Option } if (options.statistics) { - oracle = new CounterAdaptiveQueryOracle<>(oracle, "sul"); + oracle = new CounterAdaptiveQueryOracle<>(oracle, SUL_KEY); } if (options.cache) { oracle = MealyCaches.createAdaptiveQueryCache(alphabet, oracle); if (options.statistics) { - oracle = new CounterAdaptiveQueryOracle<>(oracle, "cache"); + oracle = new CounterAdaptiveQueryOracle<>(oracle, CACHE_KEY); } } return oracle; } - private static AdaptiveMembershipOracle buildSingleAdaptiveOracle(Options options, File path) { + static AdaptiveMembershipOracle buildSingleAdaptiveOracle(Options options, File path) { if (options.stdin) { return new StdInOutputAdaptiveOracle<>(buildCommandLine(options, path), Function.identity(), options.reset); } else { @@ -187,21 +189,35 @@ private static void verifyReset(Options options) { } } - class OutputTransformer implements BiFunction> { + private static class OutputTransformer implements CLIOutputOracle.OutputTransformer>, + StdInOutputOracle.OutputTransformer> { private final Pattern pattern; - public OutputTransformer(Options options) { + OutputTransformer(Options options) { this.pattern = Pattern.compile(options.delimiter); } @Override - public Word apply(String string, Integer suffixLength) { - if (string.isEmpty()) { + public Word transform(String output, int prefixLength, int suffixLength) { + if (suffixLength == 0) { return Word.epsilon(); } - final String[] orig = pattern.split(string); - return Word.fromArray(orig, orig.length - suffixLength, suffixLength); + + 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 index efed280fb..3a0dcac1e 100644 --- a/cli/src/main/java/de/learnlib/cli/factory/SerializerFactory.java +++ b/cli/src/main/java/de/learnlib/cli/factory/SerializerFactory.java @@ -16,7 +16,6 @@ package de.learnlib.cli.factory; import java.io.DataOutput; -import java.util.function.Function; import de.learnlib.cli.option.Options; import de.learnlib.cli.option.Output; @@ -35,21 +34,14 @@ import net.automatalib.serialization.mata.writer.MataNFAWriter; import net.automatalib.serialization.saf.SAFWriters; import net.automatalib.serialization.taf.writer.TAFWriters; -import net.automatalib.ts.simple.SimpleTS; -@FunctionalInterface -public interface SerializerFactory> - extends Function> { +public final class SerializerFactory { - SerializerFactory> DFA_SERIALIZER = SerializerFactory::getDFASerializer; - SerializerFactory> MEALY_SERIALIZER = SerializerFactory::getMealySerializer; - SerializerFactory> NFA_SERIALIZER = SerializerFactory::getNFASerializer; - SerializerFactory> SBA_SERIALIZER = SerializerFactory::getSBASerializer; - SerializerFactory> SPA_SERIALIZER = SerializerFactory::getSPASerializer; - SerializerFactory> SPMM_SERIALIZER = SerializerFactory::getSPMMSerializer; - SerializerFactory> VPA_SERIALIZER = SerializerFactory::getVPASerializer; + private SerializerFactory() { + // prevent instantiation + } - private static InputModelSerializer> getDFASerializer(Options options) { + public static InputModelSerializer> getDFASerializer(Options options) { final InputModelSerializer> dfaSerializer = SerializerFactory.getDFASerializerInternal(options); return dfaSerializer::writeModel; @@ -67,7 +59,7 @@ private static InputModelSerializer> getDFASerializer(Options o }; } - private static InputModelSerializer> getMealySerializer(Options options) { + public static InputModelSerializer> getMealySerializer(Options options) { return switch (options.format) { case DOT -> DOTSerializationProvider.forAutomaton(); case SAF -> SAFWriters.mealy(DataOutput::writeUTF); @@ -76,7 +68,7 @@ private static InputModelSerializer> getDFASerializer(Options o }; } - private static InputModelSerializer> getNFASerializer(Options options) { + public static InputModelSerializer> getNFASerializer(Options options) { final InputModelSerializer> nfaSerializer = SerializerFactory.getNFASerializerInternal(options); return nfaSerializer::writeModel; @@ -93,35 +85,35 @@ private static InputModelSerializer> getNFASerializer(Options o }; } - private static InputModelSerializer> getSBASerializer(Options options) { + public static InputModelSerializer> getSBASerializer(Options options) { if (options.format == Output.DOT) { return DOTSerializationProvider.forGraphViewableInput(); } throw new UnsupportedCombinationException(options); } - private static InputModelSerializer> getSPASerializer(Options options) { + public static InputModelSerializer> getSPASerializer(Options options) { if (options.format == Output.DOT) { return DOTSerializationProvider.forGraphViewableInput(); } throw new UnsupportedCombinationException(options); } - private static InputModelSerializer> getSPMMSerializer(Options options) { + public static InputModelSerializer> getSPMMSerializer(Options options) { if (options.format == Output.DOT) { return DOTSerializationProvider.forGraphViewableInput(); } throw new UnsupportedCombinationException(options); } - private static InputModelSerializer> getVPASerializer(Options options) { + public static InputModelSerializer> getVPASerializer(Options options) { if (options.format == Output.DOT) { return DOTSerializationProvider.forGraphViewableInput(); } throw new UnsupportedCombinationException(options); } - class UnsupportedCombinationException extends IllegalArgumentException { + 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 index 3cf2986a0..f837e2db0 100644 --- a/cli/src/main/java/de/learnlib/cli/option/EQOParams.java +++ b/cli/src/main/java/de/learnlib/cli/option/EQOParams.java @@ -207,7 +207,7 @@ public static class Samples { public List samples; @Option(names = "--eqo-sample-split", - defaultValue = " ", + defaultValue = "\\s", paramLabel = "", descriptionKey = "param.eqo.sample.split") public String split; diff --git a/cli/src/main/java/de/learnlib/cli/option/Options.java b/cli/src/main/java/de/learnlib/cli/option/Options.java index 70199ad2b..18151624f 100644 --- a/cli/src/main/java/de/learnlib/cli/option/Options.java +++ b/cli/src/main/java/de/learnlib/cli/option/Options.java @@ -37,7 +37,7 @@ public class Options { @Option(names = {"-r", "--reset"}, paramLabel = "", descriptionKey = "option.reset") public String reset; - @Option(names = {"-d", "--delim"}, paramLabel = "", defaultValue = "\n", descriptionKey = "option.delim") + @Option(names = {"-d", "--delim"}, paramLabel = "", defaultValue = "\\n", descriptionKey = "option.delim") public String delimiter; @Option(names = {"-e", "--eqo"}, paramLabel = "", defaultValue = "RANDOM_WP", descriptionKey = "option.eqo") diff --git a/cli/src/main/java/de/learnlib/cli/option/Type.java b/cli/src/main/java/de/learnlib/cli/option/Type.java index b55fcadeb..d2cb1ddd6 100644 --- a/cli/src/main/java/de/learnlib/cli/option/Type.java +++ b/cli/src/main/java/de/learnlib/cli/option/Type.java @@ -28,79 +28,79 @@ public enum Type { DFA { @Override public Runner runner(Options options) { - return new PresetRunner<>(AlphabetFactory.REGULAR, - MQOFactory.ACCEPTOR, - LearnerFactory.DFA_LEARNER, - EQOFactory.DFA_ORACLES, - SerializerFactory.DFA_SERIALIZER); + 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.LSHARP) { - return new AdaptiveRunner<>(AlphabetFactory.REGULAR, - MQOFactory.ADAPTIVE, - LearnerFactory.ADAPTIVE_LEARNER, - EQOFactory.ADAPTIVE_ORACLES, - SerializerFactory.MEALY_SERIALIZER); + return new AdaptiveRunner<>(AlphabetFactory::getRegularAlphabet, + MQOFactory::getAdaptiveOracle, + LearnerFactory::getAdaptiveLearner, + EQOFactory::getAdaptiveOracles, + SerializerFactory::getMealySerializer); } else { - return new PresetRunner<>(AlphabetFactory.REGULAR, - MQOFactory.TRANSDUCER, - LearnerFactory.MEALY_LEARNER, - EQOFactory.MEALY_ORACLES, - SerializerFactory.MEALY_SERIALIZER); + return new PresetRunner<>(AlphabetFactory::getRegularAlphabet, + MQOFactory::getTransducerOracle, + LearnerFactory::getMealyLearner, + EQOFactory::getRegularOracles, + SerializerFactory::getMealySerializer); } } }, NFA { @Override public Runner runner(Options options) { - return new PresetRunner<>(AlphabetFactory.REGULAR, - MQOFactory.ACCEPTOR, - LearnerFactory.NFA_LEARNER, - EQOFactory.NFA_ORACLES, - SerializerFactory.NFA_SERIALIZER); + return new PresetRunner<>(AlphabetFactory::getRegularAlphabet, + MQOFactory::getAcceptorOracle, + LearnerFactory::getNFALearner, + EQOFactory::getNFAOracles, + SerializerFactory::getNFASerializer); } }, SBA { @Override public Runner runner(Options options) { - return new PresetRunner<>(AlphabetFactory.PROCEDURAL, - MQOFactory.ACCEPTOR, - LearnerFactory.SBA_LEARNER, - EQOFactory.SBA_ORACLES, - SerializerFactory.SBA_SERIALIZER); + return new PresetRunner<>(AlphabetFactory::getProceduralAlphabet, + MQOFactory::getAcceptorOracle, + LearnerFactory::getSBALearner, + EQOFactory::getSBAOracles, + SerializerFactory::getSBASerializer); } }, SPA { @Override public Runner runner(Options options) { - return new PresetRunner<>(AlphabetFactory.PROCEDURAL, - MQOFactory.ACCEPTOR, - LearnerFactory.SPA_LEARNER, - EQOFactory.SPA_ORACLES, - SerializerFactory.SPA_SERIALIZER); + return new PresetRunner<>(AlphabetFactory::getProceduralAlphabet, + MQOFactory::getAcceptorOracle, + LearnerFactory::getSPALearner, + EQOFactory::getSPAOracles, + SerializerFactory::getSPASerializer); } }, SPMM { @Override public Runner runner(Options options) { - return new PresetRunner<>(AlphabetFactory.PROCEDURAL, - MQOFactory.TRANSDUCER, - LearnerFactory.SPMM_LEARNER, - EQOFactory.SPMM_ORACLES, - SerializerFactory.SPMM_SERIALIZER); + return new PresetRunner<>(AlphabetFactory::getProceduralAlphabet, + MQOFactory::getTransducerOracle, + LearnerFactory::getSPMMLearner, + EQOFactory::getSPMMOracles, + SerializerFactory::getSPMMSerializer); } }, VPA { @Override public Runner runner(Options options) { - return new PresetRunner<>(AlphabetFactory.VPA, - MQOFactory.ACCEPTOR, - LearnerFactory.VPA_LEARNER, - EQOFactory.VPA_ORACLES, - SerializerFactory.VPA_SERIALIZER); + return new PresetRunner<>(AlphabetFactory::getVPAlphabet, + MQOFactory::getAcceptorOracle, + LearnerFactory::getVPALearner, + EQOFactory::getVPAOracles, + SerializerFactory::getVPASerializer); } }; diff --git a/cli/src/main/java/de/learnlib/cli/util/AbstractRunner.java b/cli/src/main/java/de/learnlib/cli/util/AbstractRunner.java index 133834257..a2dfc1d00 100644 --- a/cli/src/main/java/de/learnlib/cli/util/AbstractRunner.java +++ b/cli/src/main/java/de/learnlib/cli/util/AbstractRunner.java @@ -41,6 +41,9 @@ public abstract class AbstractRunner, M extends SimpleTS alphabetCreator; private final BiFunction mqoCreator; private final Function> learnerCreator; @@ -67,7 +70,7 @@ public void run(Options options) { final OR learnerOracle; if (options.statistics) { - learnerOracle = getCounter(mqo, "learner"); + learnerOracle = getCounter(mqo, LEARNER_KEY); } else { learnerOracle = mqo; } @@ -77,7 +80,7 @@ public void run(Options options) { final OR eqoOracle; if (options.statistics) { - eqoOracle = getCounter(mqo, "eqo"); + eqoOracle = getCounter(mqo, EQO_KEY); } else { eqoOracle = mqo; } @@ -93,7 +96,6 @@ public void run(Options options) { if (options.statistics) { final StatisticsService service = Statistics.getService(); LOGGER.info(Category.STATISTIC, service.print()); - service.clear(); } try { diff --git a/cli/src/main/resources/application.properties b/cli/src/main/resources/application.properties index c3248b3ce..ea225b487 100644 --- a/cli/src/main/resources/application.properties +++ b/cli/src/main/resources/application.properties @@ -17,7 +17,7 @@ app.version=${project.version} option.args=Additional arguments to pass to the SUL(s). option.cache=Use a query cache. -option.delim=The string used to split responses of the SUL(s) into individual output symbols. +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}. @@ -61,7 +61,7 @@ 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 to split and individual symbols of a trace. +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'. diff --git a/cli/src/test/java/de/learnlib/cli/ApplicationIT.java b/cli/src/test/java/de/learnlib/cli/ApplicationIT.java index 2d57768f5..2660824f0 100644 --- a/cli/src/test/java/de/learnlib/cli/ApplicationIT.java +++ b/cli/src/test/java/de/learnlib/cli/ApplicationIT.java @@ -26,8 +26,17 @@ 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 net.automatalib.common.util.process.ProcessUtil; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.LoggerFactory; import org.testng.Assert; import org.testng.SkipException; import org.testng.annotations.BeforeTest; @@ -36,10 +45,10 @@ public class ApplicationIT { - private static final String STATELESS = getPathToScript("/suls/stateless.py"); - private static final String STATELESS_STDIN = getPathToScript("/suls/stateless_stdin.py"); - private static final String STATEFUL = getPathToScript("/suls/stateful.py"); - private static final String STATEFUL_STDIN = getPathToScript("/suls/stateful_stdin.py"); + public static final String STATELESS = getPathToScript("/sul/stateless.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)); @@ -62,76 +71,140 @@ public void setUp() { } @Test - public void testStatelessMealy() throws IOException { - checkExecution(new String[] {"-sa", "-sb", "-tMEALY", "--eqo-random-wp-bound", "100", STATELESS}, "/mealy.dot"); + 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 { - checkExecution(new String[] {"-sa", "-sb", "--eqo-random-wp-bound", "100", STATELESS}, "/dfa.dot"); + checkRegularExecution(new String[] {"-lLSTAR", "-eSAMPLE", "--eqo-sample=a b", STATELESS}, "/ser/dfa.dot"); + } + + @Test + public void testMealyPreset() throws IOException { + checkRegularExecution(new String[] {"-lLSTAR", "-tMEALY", "-eSAMPLE", "--eqo-sample=a b", STATELESS}, + "/ser/mealy.dot"); + } + + @Test + public void testMealyAdaptive() throws IOException { + checkRegularExecution(new String[] {"-tMEALY", + "-lLSHARP", + "-eSAMPLE", + "--eqo-sample=a b", + "--reset=reset", + STATEFUL}, "/ser/mealy.dot"); + } + + @Test + public void testStatelessNFA() throws IOException { + checkRegularExecution(new String[] {"-tNFA", "-lNLSTAR", "-eSAMPLE", "--eqo-sample=a b", STATELESS}, + "/ser/dfa.dot"); } @Test - public void testStatelessStdInMealy() throws IOException { - checkExecution(new String[] {"-sa", "-sb", "-tMEALY", "--stdin", "-eWP", STATELESS_STDIN}, "/mealy.dot"); + public void testStatelessSBA() throws IOException { + checkProceduralExecution(new String[] {"-lLSTAR", "-tSBA", "-eSAMPLE", "--eqo-sample=S a R", STATELESS_SBA}, + null); } @Test - public void testStatelessStdInDFA() throws IOException { - checkExecution(new String[] {"-sa", "-sb", "--stdin", "-eWP", STATELESS_STDIN}, "/dfa.dot"); + public void testStatelessSPA() throws IOException { + checkProceduralExecution(new String[] {"-lLSTAR", "-tSPA", "-eSAMPLE", "--eqo-sample=S a R", STATELESS_SPA}, + "/ser/spa.dot"); } @Test - public void testStatefulMealy() throws IOException { - checkExecution(new String[] {"-sa", - "-sb", - "-tMEALY", - "--reset", - "reset", - "-eSAMPLE", - "--eqo-sample", - "abab", - STATEFUL}, "/mealy.dot"); + public void testStatelessSPMM() throws IOException { + checkProceduralExecution(new String[] {"-lLSTAR", + "-tSPMM", + "-eSAMPLE", + "--eqo-sample=S a R", + "-d=\\s", + STATELESS_SBA}, "/ser/spmm.dot"); } @Test - public void testStatefulDFA() throws IOException { - checkExecution(new String[] {"-sa", "-sb", "--reset", "reset", "-eSAMPLE", "--eqo-sample", "abab", STATEFUL}, - "/dfa.dot"); + public void testStatelessVPA() throws IOException { + checkProceduralExecution(new String[] {"-tVPA", "-eSAMPLE", "--eqo-sample=S a R", STATELESS_SPA}, null); } @Test - public void testStatefulStdInMealy() throws IOException { - checkExecution(new String[] {"-sa", - "-sb", - "-tMEALY", - "--stdin", - "--reset", - "reset", - "-eSAMPLE", - "--eqo-sample", - "abab", - STATEFUL_STDIN}, "/mealy.dot"); + public void testStatistics() throws IOException { + checkRegularExecution(new String[] {"-lLSTAR", "-eSAMPLE", "--eqo-sample=abab", "--stats", STATELESS}, 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 testStatefulStdInDFA() throws IOException { - checkExecution(new String[] {"-sa", - "-sb", - "--stdin", - "--reset", - "reset", - "-eSAMPLE", - "--eqo-sample", - "abab", - STATEFUL_STDIN}, "/dfa.dot"); - } - - private void checkExecution(String[] params, String resource) throws IOException { + public void testAdaptiveStatistics() throws IOException { + checkRegularExecution(new String[] {"-tMEALY", + "-lLSHARP", + "-eSAMPLE", + "--eqo-sample=abab", + "--reset=reset", + "--stats", + STATEFUL}, 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 + 2); + 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(); @@ -141,7 +214,9 @@ private void checkExecution(String[] params, String resource) throws IOException final int exitCode = cmd.execute(args); Assert.assertEquals(exitCode, 0); - checkOutputs(out, resource); + if (resource != null) { + checkOutputs(out, resource); + } } private void checkOutputs(Path output, String resource) throws IOException { 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..d5ccf45ca --- /dev/null +++ b/cli/src/test/java/de/learnlib/cli/factory/LearnerFactoryTest.java @@ -0,0 +1,489 @@ +/* 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.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.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 KV -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-lKV"}, KearnsVaziraniDFA.class}; + case LLAMBDA -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-lLLAMBDA"}, LLambdaDFA.class}; + case LSHARP -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-lLSHARP"}, Exception.class}; + case LSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-lLSTAR"}, + ExtensibleLStarDFA.class}; + case NLSTAR -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-lNLSTAR"}, Exception.class}; + case OP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-lOP"}, OPLearnerDFA.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 TTTLAMBDA -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-lTTTLAMBDA"}, 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 KV -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lKV"}, + KearnsVaziraniMealy.class}; + case LLAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lLLAMBDA"}, + LLambdaMealy.class}; + case LSHARP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lLSHARP"}, + Exception.class}; + case LSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lLSTAR"}, + ExtensibleLStarMealy.class}; + case NLSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lNLSTAR"}, + Exception.class}; + case OP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lOP"}, + OPLearnerMealy.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 TTTLAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lTTTLAMBDA"}, + 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 KV -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lKV"}, + Exception.class}; + case LLAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lLLAMBDA"}, + Exception.class}; + case LSHARP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lLSHARP"}, + LSharpMealy.class}; + case LSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lLSTAR"}, + Exception.class}; + case NLSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lNLSTAR"}, + Exception.class}; + case OP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lOP"}, + 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 TTTLAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lTTTLAMBDA"}, + 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 KV -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lKV"}, Exception.class}; + case LLAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lLLAMBDA"}, + Exception.class}; + case LSHARP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lLSHARP"}, + Exception.class}; + case LSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lLSTAR"}, + Exception.class}; + case NLSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lNLSTAR"}, + NLStarLearner.class}; + case OP -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lOP"}, 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 TTTLAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lTTTLAMBDA"}, + 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 KV -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSBA", "-lKV"}, SBALearner.class}; + case LLAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSBA", "-lLLAMBDA"}, + SBALearner.class}; + case LSHARP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSBA", "-lLSHARP"}, + Exception.class}; + case LSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSBA", "-lLSTAR"}, + SBALearner.class}; + case NLSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSBA", "-lNLSTAR"}, + Exception.class}; + case OP -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSBA", "-lOP"}, 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 TTTLAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSBA", "-lTTTLAMBDA"}, + 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 KV -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPA", "-lKV"}, SPALearner.class}; + case LLAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPA", "-lLLAMBDA"}, + SPALearner.class}; + case LSHARP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPA", "-lLSHARP"}, + Exception.class}; + case LSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPA", "-lLSTAR"}, + SPALearner.class}; + case NLSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPA", "-lNLSTAR"}, + Exception.class}; + case OP -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPA", "-lOP"}, 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 TTTLAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPA", "-lTTTLAMBDA"}, + 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 KV -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lKV"}, + SPMMLearner.class}; + case LLAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lLLAMBDA"}, + SPMMLearner.class}; + case LSHARP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lLSHARP"}, + Exception.class}; + case LSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lLSTAR"}, + SPMMLearner.class}; + case NLSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lNLSTAR"}, + Exception.class}; + case OP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lOP"}, + 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 TTTLAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lTTTLAMBDA"}, + 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 KV -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lKV"}, Exception.class}; + case LLAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lLLAMBDA"}, + Exception.class}; + case LSHARP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lLSHARP"}, + Exception.class}; + case LSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lLSTAR"}, + Exception.class}; + case NLSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lNLSTAR"}, + Exception.class}; + case OP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lOP"}, + OPLearnerVPA.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 TTTLAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lTTTLAMBDA"}, + 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..faf4f2a50 --- /dev/null +++ b/cli/src/test/java/de/learnlib/cli/factory/MQOFactoryTest.java @@ -0,0 +1,329 @@ +/* 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.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 { + + 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(oracleMock); + 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(oracleMock); + 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(oracleMock); + 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 URL resource = MQOFactoryTest.class.getResource("/sul/stateless_broken.py"); + Assert.assertNotNull(resource); + File sulFile = new File(resource.getFile()); + + final Options options = new Options(); + options.delimiter = "\\n"; + + 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..6611f17a0 --- /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 + + + + + + + \ No newline at end of file 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/dfa.dot b/cli/src/test/resources/ser/dfa.dot similarity index 71% rename from cli/src/test/resources/dfa.dot rename to cli/src/test/resources/ser/dfa.dot index 356623100..66dbba275 100644 --- a/cli/src/test/resources/dfa.dot +++ b/cli/src/test/resources/ser/dfa.dot @@ -1,7 +1,7 @@ digraph g { - s0 [shape="circle" label="s0"]; - s1 [shape="doublecircle" label="s1"]; + s0 [shape="circle" label="0"]; + s1 [shape="doublecircle" label="1"]; s0 -> s1 [label="a"]; s0 -> s1 [label="b"]; s1 -> s0 [label="a"]; 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 0000000000000000000000000000000000000000..3fb36f2da552e831ebac9e67a06e8cf056edc95d GIT binary patch literal 36 WcmWG`bYlPlCMbrn8G#HC1_J;dqX6Ro literal 0 HcmV?d00001 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/mealy.dot b/cli/src/test/resources/ser/mealy.dot similarity index 81% rename from cli/src/test/resources/mealy.dot rename to cli/src/test/resources/ser/mealy.dot index 89539b79e..7e910c54f 100644 --- a/cli/src/test/resources/mealy.dot +++ b/cli/src/test/resources/ser/mealy.dot @@ -1,6 +1,6 @@ digraph g { - s0 [shape="circle" label="s0"]; + s0 [shape="circle" label="0"]; s0 -> s0 [label="a / 97"]; s0 -> s0 [label="b / 98"]; diff --git a/cli/src/test/resources/ser/mealy.saf b/cli/src/test/resources/ser/mealy.saf new file mode 100644 index 0000000000000000000000000000000000000000..4469cf8d74fef2d2f494ee31f389027055505eb5 GIT binary patch literal 32 bcmWG`bYo&*U;tr8Acg=YOLH)T$ s0 + a / 97 -> 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 0000000000000000000000000000000000000000..6e4ad07642942e3d264a0ee045f003947f89a26a GIT binary patch literal 56 ccmWG`bYo;-U|@n`5E}$Qd=MKQgXF+!05IhM=>Px# literal 0 HcmV?d00001 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..2d7f96767 --- /dev/null +++ b/cli/src/test/resources/sul/sba.py @@ -0,0 +1,40 @@ +#!/bin/python +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..856b8805e --- /dev/null +++ b/cli/src/test/resources/sul/spa.py @@ -0,0 +1,36 @@ +#!/bin/python +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/suls/stateful.py b/cli/src/test/resources/sul/stateful.py similarity index 100% rename from cli/src/test/resources/suls/stateful.py rename to cli/src/test/resources/sul/stateful.py diff --git a/cli/src/test/resources/suls/stateless.py b/cli/src/test/resources/sul/stateless.py similarity index 100% rename from cli/src/test/resources/suls/stateless.py rename to cli/src/test/resources/sul/stateless.py diff --git a/cli/src/test/resources/suls/stateless_stdin.py b/cli/src/test/resources/sul/stateless_broken.py similarity index 67% rename from cli/src/test/resources/suls/stateless_stdin.py rename to cli/src/test/resources/sul/stateless_broken.py index 3587023cd..d3106c5ef 100755 --- a/cli/src/test/resources/suls/stateless_stdin.py +++ b/cli/src/test/resources/sul/stateless_broken.py @@ -4,9 +4,8 @@ def main(): argv = len(sys.argv) - for line in sys.stdin: - for arg in line.split(): - argv += 1 + if argv > 2: + for arg in sys.argv[2:]: print(sum([ord(c) for c in arg])) sys.exit(argv % 2) diff --git a/cli/src/test/resources/suls/stateful_stdin.py b/cli/src/test/resources/suls/stateful_stdin.py deleted file mode 100755 index 277c9caa4..000000000 --- a/cli/src/test/resources/suls/stateful_stdin.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/python -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])) - - pickle.dump(argv, open(path, "wb")) - sys.exit(argv % 2) - -if __name__ == "__main__": - main() 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/src/main/java/de/learnlib/oracle/membership/CLIOutputOracle.java b/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/CLIOutputOracle.java index 3193f0ca9..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; @@ -55,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; /** @@ -65,11 +64,11 @@ public class CLIOutputOracle 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 suffix for properly offsetting potentially {@link Word}-based output types. + * 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); } @@ -80,13 +79,11 @@ public CLIOutputOracle(List commandLine, BiFunction * 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 suffix for properly offsetting potentially {@link Word}-based output types. + * 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; @@ -119,7 +116,7 @@ private D answerStatelessQuery(Word prefix, Word suffix) { logInvocation(args); ProcessUtil.invokeProcess(args, sj::add, LOGGER::warn); logResult(sj); - return outputTransformer.apply(sj.toString(), suffix.length()); + return outputTransformer.transform(sj.toString(), prefix.length(), suffix.length()); } catch (IOException | InterruptedException e) { throw new IllegalStateException(e); } @@ -143,7 +140,7 @@ private D answerStatefulQuery(Word prefix, Word suffix) { answerStatefulSymbol(s, sj); } - return outputTransformer.apply(sj.toString(), suffix.length()); + return outputTransformer.transform(sj.toString(), prefix.length(), suffix.length()); } catch (IOException | InterruptedException e) { throw new IllegalStateException(e); } @@ -163,4 +160,29 @@ private static void logInvocation(String[] 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/StdInOutputOracle.java b/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/StdInOutputOracle.java index 752b9b509..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 @@ -19,7 +19,6 @@ import java.io.StringReader; import java.util.List; import java.util.StringJoiner; -import java.util.function.BiFunction; import de.learnlib.oracle.SingleQueryOracle; import net.automatalib.common.setting.AutomataLibProperty; @@ -59,7 +58,7 @@ public class StdInOutputOracle implements SingleQueryOracle { AutomataLibSettings.getInstance().getProperty(AutomataLibProperty.WORD_SYMBOL_SEPARATOR, " "); private final List commandLine; - private final BiFunction outputTransformer; + private final OutputTransformer outputTransformer; private final @Nullable String reset; /** @@ -69,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 suffix 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); } @@ -84,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; @@ -120,7 +117,7 @@ private D answerStatelessQuery(Word prefix, Word suffix) { ProcessUtil.invokeProcess(commandLine, new StringReader(input.toString()), sj::add, LOGGER::warn); logResult(sj); - return outputTransformer.apply(sj.toString(), suffix.length()); + return outputTransformer.transform(sj.toString(), prefix.length(), suffix.length()); } catch (IOException | InterruptedException e) { throw new IllegalStateException(e); } @@ -143,7 +140,7 @@ private D answerStatefulQuery(Word prefix, Word suffix) { answerStatefulSymbol(s, sj); } - return outputTransformer.apply(sj.toString(), suffix.length()); + return outputTransformer.transform(sj.toString(), prefix.length(), suffix.length()); } catch (IOException | InterruptedException e) { throw new IllegalStateException(e); } @@ -162,4 +159,29 @@ private static void logInvocation(List command, Object 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/test/java/de/learnlib/oracle/membership/CLIOutputOracleTest.java b/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/CLIOutputOracleTest.java index 7aa19928e..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 @@ -61,7 +61,7 @@ public void testStatefulCommunication() throws URISyntaxException { Assert.assertThrows(() -> brokenOracle.answerQuery(Word.fromLetter('a'), Word.fromString("ab"))); } - static Word parseOutput(String input, Integer suffix) { + static Word parseOutput(String input, int prefix, int suffix) { if (suffix == 0) { return Word.epsilon(); } @@ -73,6 +73,10 @@ static Word parseOutput(String input, Integer suffix) { final Word result = Arrays.stream(input.split(System.lineSeparator())).map(Integer::parseInt).collect(Word.collector()); - return result.subWord(result.size() - suffix); + if (result.length() != (prefix + suffix)) { + throw new IllegalStateException(); + } + + return result.suffix(suffix); } } From 285db43aec11ed692787d65d98a5f354f4bf99cf Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Thu, 30 Jul 2026 00:23:02 +0200 Subject: [PATCH 17/34] add support for snapshotting --- CHANGELOG.md | 4 +- .../algorithm/aaar/AbstractAAARTest.java | 6 +- .../de/learnlib/algorithm/adt/it/ADTIT.java | 2 +- .../AbstractCounterexampleQueueTest.java | 5 +- .../lambda/lstar/mealy/it/LLambdaMealyIT.java | 2 +- .../learnlib/algorithm/nlstar/NLStarTest.java | 3 +- .../vpa/DTVisualizationTest.java | 2 +- .../procedural/sba/OptimizationsTest.java | 2 +- .../procedural/spmm/OptimizationsTest.java | 3 +- .../src/main/java/Example.java | 2 +- .../src/main/java/Example.java | 2 +- build-parent/pom.xml | 3 + cli/pom.xml | 4 + .../java/de/learnlib/cli/Application.java | 4 +- .../java/de/learnlib/cli/option/Options.java | 7 + .../de/learnlib/cli/util/AbstractRunner.java | 25 +- .../cli/util/SnapshottingExperiment.java | 122 +++++++++ cli/src/main/java/module-info.java | 1 + cli/src/main/resources/application.properties | 2 + .../java/de/learnlib/cli/ApplicationIT.java | 4 +- .../test/java/de/learnlib/cli/SnapshotIT.java | 182 +++++++++++++ .../learnlib/cli/factory/MQOFactoryTest.java | 11 +- cli/src/test/resources/ser/mealy_large.dot | 19 ++ cli/src/test/resources/sul/stateless_large.py | 21 ++ commons/util/pom.xml | 6 + .../java/de/learnlib/util/Experiment.java | 253 ++++++++++-------- commons/util/src/main/java/module-info.java | 1 + .../java/de/learnlib/util/ExperimentTest.java | 1 - examples/pom.xml | 4 +- .../java/de/learnlib/example/Example1.java | 2 +- .../java/de/learnlib/example/Example2.java | 2 +- .../learnlib/example/bbc/LTSminExample1.java | 2 +- .../learnlib/example/bbc/LTSminExample2.java | 2 +- .../learnlib/example/bbc/LTSminExample3.java | 2 +- .../learnlib/example/bbc/LTSminExample4.java | 2 +- .../example/resumable/ResumableExample.java | 19 +- .../de/learnlib/example/sli/Example1.java | 2 +- .../de/learnlib/example/sli/Example2.java | 2 +- examples/src/main/java/module-info.java | 2 +- pom.xml | 12 +- test-support/test-support/pom.xml | 4 +- .../de/learnlib/testsupport/ResumeUtils.java | 13 +- .../src/main/java/module-info.java | 2 +- 43 files changed, 596 insertions(+), 175 deletions(-) create mode 100644 cli/src/main/java/de/learnlib/cli/util/SnapshottingExperiment.java create mode 100644 cli/src/test/java/de/learnlib/cli/SnapshotIT.java create mode 100644 cli/src/test/resources/ser/mealy_large.dot create mode 100755 cli/src/test/resources/sul/stateless_large.py diff --git a/CHANGELOG.md b/CHANGELOG.md index cc046ef9d..0b72e2d87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,12 +21,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/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..1db74d04b 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -80,6 +80,9 @@ 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/DFAExample.class + de/learnlib/util/MealyExample.class + de/learnlib/util/MooreExample.class diff --git a/cli/pom.xml b/cli/pom.xml index 597571de7..570ded687 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -174,6 +174,10 @@ limitations under the License. info.picocli picocli + + org.apache.fory + fory-core + org.slf4j slf4j-api diff --git a/cli/src/main/java/de/learnlib/cli/Application.java b/cli/src/main/java/de/learnlib/cli/Application.java index 8dbe3c301..82bf9e5fe 100644 --- a/cli/src/main/java/de/learnlib/cli/Application.java +++ b/cli/src/main/java/de/learnlib/cli/Application.java @@ -33,7 +33,7 @@ descriptionHeading = "%nDescription:%n%n", parameterListHeading = "%nParameters:%n", optionListHeading = "%nOptions:%n", - description = "Runs an active automata learning process by invoking the provided SUL 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/.") + 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"; @@ -67,6 +67,4 @@ private void setLogLevel(Options options) { root.setLevel(level); } } - } - diff --git a/cli/src/main/java/de/learnlib/cli/option/Options.java b/cli/src/main/java/de/learnlib/cli/option/Options.java index 18151624f..7b3cc2932 100644 --- a/cli/src/main/java/de/learnlib/cli/option/Options.java +++ b/cli/src/main/java/de/learnlib/cli/option/Options.java @@ -23,6 +23,7 @@ import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; +@SuppressWarnings("PMD.TooManyFields") public class Options { @Parameters(paramLabel = "", descriptionKey = "option.sul", arity = "1..*") @@ -61,6 +62,12 @@ public class Options { @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; diff --git a/cli/src/main/java/de/learnlib/cli/util/AbstractRunner.java b/cli/src/main/java/de/learnlib/cli/util/AbstractRunner.java index a2dfc1d00..0891621b8 100644 --- a/cli/src/main/java/de/learnlib/cli/util/AbstractRunner.java +++ b/cli/src/main/java/de/learnlib/cli/util/AbstractRunner.java @@ -22,6 +22,7 @@ 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; @@ -87,11 +88,9 @@ public void run(Options options) { final EquivalenceOracle eqo = eqoCreator.apply(options, eqoOracle); final InputModelSerializer serializer = serializerCreator.apply(options); + final Experiment experiment = buildExperiment(learner, eqo, alphabet, serializer, options); - final Experiment experiment = new Experiment<>(learner, eqo, alphabet, serializer); - experiment.run(); - - final M hyp = experiment.getFinalHypothesis(); + final M hyp = experiment.run(); if (options.statistics) { final StatisticsService service = Statistics.getService(); @@ -111,5 +110,23 @@ public void run(Options options) { } } + 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/SnapshottingExperiment.java b/cli/src/main/java/de/learnlib/cli/util/SnapshottingExperiment.java new file mode 100644 index 000000000..bcdd7afc7 --- /dev/null +++ b/cli/src/main/java/de/learnlib/cli/util/SnapshottingExperiment.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.util; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDate; +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.annotation.Nullable; +import org.apache.fory.exception.ForyException; + +final class SnapshottingExperiment extends Experiment { + + private static final Fory FORY; + private static final DateTimeFormatter DTF; + + static { + FORY = Fory.builder() + .requireClassRegistration(false) + .withCodegen(false) + .withRefTracking(true) + .withXlang(false) + .build(); + 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(LocalDate.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(); + } + } + } + + 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); + } + } + + 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/module-info.java b/cli/src/main/java/module-info.java index 129bcada3..3509f3660 100644 --- a/cli/src/main/java/module-info.java +++ b/cli/src/main/java/module-info.java @@ -51,6 +51,7 @@ requires ch.qos.logback.classic; requires info.picocli; + requires org.apache.fory.core; requires org.slf4j; } diff --git a/cli/src/main/resources/application.properties b/cli/src/main/resources/application.properties index ea225b487..e02ffdec6 100644 --- a/cli/src/main/resources/application.properties +++ b/cli/src/main/resources/application.properties @@ -23,6 +23,8 @@ option.format=The output format to use for printing the final hypothesis model. 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. diff --git a/cli/src/test/java/de/learnlib/cli/ApplicationIT.java b/cli/src/test/java/de/learnlib/cli/ApplicationIT.java index 2660824f0..905f9b274 100644 --- a/cli/src/test/java/de/learnlib/cli/ApplicationIT.java +++ b/cli/src/test/java/de/learnlib/cli/ApplicationIT.java @@ -46,6 +46,8 @@ public class ApplicationIT { 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"); @@ -219,7 +221,7 @@ private void checkProceduralExecution(String[] params, String resource) throws I } } - private void checkOutputs(Path output, String resource) throws IOException { + public static void checkOutputs(Path output, String resource) throws IOException { final StringWriter expectedWriter = new StringWriter(); try (Reader reader = IOUtil.asBufferedUTF8Reader(ApplicationIT.class.getResourceAsStream(resource))) { 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..4725974f5 --- /dev/null +++ b/cli/src/test/java/de/learnlib/cli/SnapshotIT.java @@ -0,0 +1,182 @@ +/* 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); + 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.STATELESS, + "-sa", + "-sb", + "-tMEALY", + "-lSPARSE", + "--resume-from", + resume.toString()); + Assert.assertTrue(exitCode > 0); + + exitCode = cmd.execute(ApplicationIT.STATELESS, + "-sa", + "-sb", + "-tMEALY", + "-lSPARSE", + "--snapshot-dir", + snapshot.toString()); + Assert.assertTrue(exitCode > 0); + + exitCode = cmd.execute(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.STATELESS, "-sa", "-sb", "--resume-from", resume.toString()); + Assert.assertTrue(exitCode > 0); + + exitCode = cmd.execute(ApplicationIT.STATELESS, "-sa", "-sb", "--snapshot-dir", snapshot.toString()); + Assert.assertTrue(exitCode > 0); + + exitCode = cmd.execute(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.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.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.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/MQOFactoryTest.java b/cli/src/test/java/de/learnlib/cli/factory/MQOFactoryTest.java index faf4f2a50..3ef6effab 100644 --- a/cli/src/test/java/de/learnlib/cli/factory/MQOFactoryTest.java +++ b/cli/src/test/java/de/learnlib/cli/factory/MQOFactoryTest.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.function.BiFunction; +import de.learnlib.cli.ApplicationIT; import de.learnlib.cli.option.Options; import de.learnlib.cli.util.AcceptorNullOracle; import de.learnlib.cli.util.AdaptiveNullOracle; @@ -166,7 +167,7 @@ private > void testOracle(Verification Statistics.getService().clear(); // with cache - Mockito.reset(oracleMock); + Mockito.reset(new Object[]{oracleMock}); // make compiler happy options.cache = true; oracle = oracleFunction.apply(options, alphabet); @@ -184,7 +185,7 @@ private > void testOracle(Verification Statistics.getService().clear(); // with stats - Mockito.reset(oracleMock); + Mockito.reset(new Object[]{oracleMock}); // make compiler happy options.cache = false; options.statistics = true; oracle = oracleFunction.apply(options, alphabet); @@ -203,7 +204,7 @@ private > void testOracle(Verification Statistics.getService().clear(); // with stats + cache - Mockito.reset(oracleMock); + Mockito.reset(new Object[]{oracleMock}); // make compiler happy options.cache = true; oracle = oracleFunction.apply(options, alphabet); @@ -313,9 +314,7 @@ public void testAdaptiveOracle() { @Test public void testOutputTransformer() { - final URL resource = MQOFactoryTest.class.getResource("/sul/stateless_broken.py"); - Assert.assertNotNull(resource); - File sulFile = new File(resource.getFile()); + File sulFile = new File(ApplicationIT.STATELESS_BROKEN); final Options options = new Options(); options.delimiter = "\\n"; 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/sul/stateless_large.py b/cli/src/test/resources/sul/stateless_large.py new file mode 100755 index 000000000..2fe05c50d --- /dev/null +++ b/cli/src/test/resources/sul/stateless_large.py @@ -0,0 +1,21 @@ +#!/bin/python +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 559a2b8e7..b731acd48 100644 --- a/commons/util/src/main/java/de/learnlib/util/Experiment.java +++ b/commons/util/src/main/java/de/learnlib/util/Experiment.java @@ -20,12 +20,21 @@ 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; @@ -42,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 @@ -68,8 +116,14 @@ 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; + protected static final Logger LOGGER = LoggerFactory.getLogger(Experiment.class); + + 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; /** @@ -83,16 +137,12 @@ public class Experiment { * the strategy for finding counterexamples * @param inputs * the inputs to consider for exploration - * @param - * input symbol type - * @param - * output domain type * * @see Experiment#Experiment(LearningAlgorithm, EquivalenceOracle, Alphabet, InputModelSerializer) */ - public Experiment(LearningAlgorithm learningAlgorithm, - EquivalenceOracle equivalenceAlgorithm, - Alphabet inputs) { + public Experiment(LearningAlgorithm learningAlgorithm, + EquivalenceOracle equivalenceAlgorithm, + Alphabet inputs) { this(learningAlgorithm, equivalenceAlgorithm, inputs, null); } @@ -108,152 +158,123 @@ public Experiment(LearningAlgorithm learningAlgorithm, * @param serializer * the serializer for logging intermediate hypotheses (may be {@code null} in case no such logging is * wanted) - * @param - * input symbol type - * @param - * output domain type */ - public Experiment(LearningAlgorithm learningAlgorithm, - EquivalenceOracle equivalenceAlgorithm, - Alphabet inputs, - @Nullable InputModelSerializer serializer) { - this.impl = new ExperimentImpl<>(learningAlgorithm, equivalenceAlgorithm, inputs, serializer); + 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(); } /** - * Run the experiment, once. + * Returns the final hypothesis model. * - * @return the final hypothesis + * @return the final hypothesis model * * @throws IllegalStateException - * if invoked more than once + * if the experiment has not been run yet */ - public A run() { - if (this.finalHypothesis != null) { - throw new IllegalStateException("Experiment has already been run"); + public final A getFinalHypothesis() { + if (finalHypothesis == null) { + throw new IllegalStateException("Experiment has not yet been run"); } - finalHypothesis = impl.run(); return finalHypothesis; } /** - * Returns the final hypothesis model. + * Run the experiment, once. * - * @return the final hypothesis model + * @return the final hypothesis * * @throws IllegalStateException - * if the experiment has not been run yet + * if invoked more than once */ - public A getFinalHypothesis() { - if (finalHypothesis == null) { - throw new IllegalStateException("Experiment has not yet been run"); + public final A run() { + if (this.finalHypothesis != null) { + throw new IllegalStateException("Experiment has already been run"); } + finalHypothesis = runInternal(); return finalHypothesis; } - private final class ExperimentImpl { + private A runInternal() { + rounds++; + statistics.increaseCounter(KEY_ROUNDS, this); + LOGGER.info(Category.PHASE, "Starting round {}", rounds); + LOGGER.info(Category.PHASE, "Learning"); - private final LearningAlgorithm learningAlgorithm; - private final EquivalenceOracle equivalenceAlgorithm; - private final Alphabet inputs; - private final @Nullable InputModelSerializer serializer; - private final StatisticsService statistics; - private int rounds; - - ExperimentImpl(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(); - } - - A run() { - rounds++; - statistics.increaseCounter(KEY_ROUNDS, Experiment.this); - LOGGER.info(Category.PHASE, "Starting round {}", rounds); - LOGGER.info(Category.PHASE, "Learning"); + initializeLearning(); - statistics.startOrResumeClock(KEY_DUR_LEARN, Experiment.this); - learningAlgorithm.startLearning(); - statistics.pauseClock(KEY_DUR_LEARN, Experiment.this); + while (true) { + final A hyp = learningAlgorithm.getHypothesisModel(); - while (true) { - final A hyp = learningAlgorithm.getHypothesisModel(); - - 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); - } + 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.PHASE, "Searching for counterexample"); - - statistics.startOrResumeClock(KEY_DUR_CEX, Experiment.this); - DefaultQuery ce = equivalenceAlgorithm.findCounterExample(hyp, inputs); - statistics.pauseClock(KEY_DUR_CEX, Experiment.this); - - if (ce == null) { - statistics.setCounter(KEY_FINAL_SIZE, hyp.size(), Experiment.this); - return hyp; - } - - 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, - Alphabet 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, - Alphabet inputs) { - super(learningAlgorithm, equivalenceAlgorithm, inputs); + postRefinementHook(); } - } - public static class MooreExperiment extends Experiment> { - - public MooreExperiment(LearningAlgorithm, I, Word> learningAlgorithm, - EquivalenceOracle, I, Word> equivalenceAlgorithm, - Alphabet 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/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/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/pom.xml b/pom.xml index b7c955147..cf4be6687 100644 --- a/pom.xml +++ b/pom.xml @@ -276,7 +276,7 @@ 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 @@ -654,9 +654,9 @@ limitations under the License. - org.apache.fury - fury-core - ${fury.version} + org.apache.fory + fory-core + ${fory.version} @@ -932,12 +932,16 @@ limitations under the License. org.apache.maven.plugins maven-resources-plugin ${resources-plugin.version} + + ${project.build.sourceEncoding} + org.apache.maven.plugins maven-compiler-plugin ${compiler-plugin.version} + -Xlint:unchecked de.learnlib.tooling 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..a7955ca58 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,24 @@ 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; /** * Utility functions for {@link Resumable} features. */ public final class ResumeUtils { - private static final Fury FURY; + private static final Fory FURY; static { LoggerFactory.useSlf4jLogging(true); - FURY = Fury.builder().withRefTracking(true).requireClassRegistration(false).build(); + FURY = Fory.builder() + .requireClassRegistration(false) + .withCodegen(false) + .withRefTracking(true) + .withXlang(false) + .build(); } private ResumeUtils() { 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; From 696e39bd8fd706bc7ad9f162ba19d83409ea37d3 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Thu, 30 Jul 2026 22:02:10 +0200 Subject: [PATCH 18/34] use LocalDateTime --- .../java/de/learnlib/cli/util/SnapshottingExperiment.java | 4 ++-- cli/src/test/java/de/learnlib/cli/SnapshotIT.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/src/main/java/de/learnlib/cli/util/SnapshottingExperiment.java b/cli/src/main/java/de/learnlib/cli/util/SnapshottingExperiment.java index bcdd7afc7..f1ff0f2c0 100644 --- a/cli/src/main/java/de/learnlib/cli/util/SnapshottingExperiment.java +++ b/cli/src/main/java/de/learnlib/cli/util/SnapshottingExperiment.java @@ -18,7 +18,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import de.learnlib.Resumable; @@ -63,7 +63,7 @@ final class SnapshottingExperiment extends super(learningAlgorithm, equivalenceAlgorithm, inputs, serializer); this.resumable = resumable; - this.fingerPrint = DTF.format(LocalDate.now()); + this.fingerPrint = DTF.format(LocalDateTime.now()); this.resumeFrom = options.resumeFrom; this.snapshotDir = options.snapshotDir; diff --git a/cli/src/test/java/de/learnlib/cli/SnapshotIT.java b/cli/src/test/java/de/learnlib/cli/SnapshotIT.java index 4725974f5..faf9a3956 100644 --- a/cli/src/test/java/de/learnlib/cli/SnapshotIT.java +++ b/cli/src/test/java/de/learnlib/cli/SnapshotIT.java @@ -41,7 +41,7 @@ private void prepareCommandLine(CommandLine cmd) { final IExecutionExceptionHandler defaultHandler = cmd.getExecutionExceptionHandler(); cmd.setExecutionExceptionHandler((ex, commandLine, fullParseResult) -> { - Assert.assertTrue(ex instanceof IllegalArgumentException); + Assert.assertTrue(ex instanceof IllegalArgumentException, ex.toString()); return defaultHandler.handleExecutionException(ex, commandLine, fullParseResult); }); cmd.setParameterExceptionHandler((ex, args) -> { From ba9fcb6fce40759c6493f0e5769fa7821830c5c9 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Thu, 30 Jul 2026 22:02:28 +0200 Subject: [PATCH 19/34] simplify --eqo-* param names --- .../de/learnlib/cli/option/EQOParams.java | 82 +++++++++---------- cli/src/main/resources/application.properties | 46 +++++------ .../learnlib/cli/factory/EQOFactoryTest.java | 42 +++++----- 3 files changed, 82 insertions(+), 88 deletions(-) diff --git a/cli/src/main/java/de/learnlib/cli/option/EQOParams.java b/cli/src/main/java/de/learnlib/cli/option/EQOParams.java index f837e2db0..6f422e9f0 100644 --- a/cli/src/main/java/de/learnlib/cli/option/EQOParams.java +++ b/cli/src/main/java/de/learnlib/cli/option/EQOParams.java @@ -44,79 +44,73 @@ public class EQOParams { public static class KWayStateMethod { - @Option(names = "--eqo-kway-s-k", - defaultValue = "2", - paramLabel = "", - descriptionKey = "param.eqo.kway-s.k") + @Option(names = "--eqo-kways-k", defaultValue = "2", paramLabel = "", descriptionKey = "param.eqo.kways.k") public int k; - @Option(names = "--eqo-kway-s-randomWalkLen", + @Option(names = "--eqo-kways-randomWalkLen", defaultValue = "20", paramLabel = "", - descriptionKey = "param.eqo.kway-s.randomWalkLen") + descriptionKey = "param.eqo.kways.randomWalkLen") public int randomWalkLen; - @Option(names = "--eqo-kway-s-combinationMethod", + @Option(names = "--eqo-kways-combinationMethod", defaultValue = "PERMUTATIONS", paramLabel = "", - descriptionKey = "param.eqo.kway-s.combinationMethod") + descriptionKey = "param.eqo.kways.combinationMethod") public CombinationMethod combinationMethod; - @Option(names = "--eqo-kway-s-seed", + @Option(names = "--eqo-kways-seed", defaultValue = "42", paramLabel = "", - descriptionKey = "param.eqo.kway-s.seed") + descriptionKey = "param.eqo.kways.seed") public int seed; } public static class KWayTransitionMethod { - @Option(names = "--eqo-kway-t-k", - defaultValue = "2", - paramLabel = "", - descriptionKey = "param.eqo.kway-t.k") + @Option(names = "--eqo-kwayt-k", defaultValue = "2", paramLabel = "", descriptionKey = "param.eqo.kwayt.k") public int k; - @Option(names = "--eqo-kway-t-randomWalkLen", + @Option(names = "--eqo-kwayt-randomWalkLen", defaultValue = "10", paramLabel = "", - descriptionKey = "param.eqo.kway-t.randomWalkLen") + descriptionKey = "param.eqo.kwayt.randomWalkLen") public int randomWalkLen; - @Option(names = "--eqo-kway-t-numGeneratePaths", + @Option(names = "--eqo-kwayt-numGeneratePaths", defaultValue = "100", paramLabel = "", - descriptionKey = "param.eqo.kway-t.numGeneratePaths") + descriptionKey = "param.eqo.kwayt.numGeneratePaths") public int numGeneratePaths; - @Option(names = "--eqo-kway-t-maxPathLen", + @Option(names = "--eqo-kwayt-maxPathLen", defaultValue = "50", paramLabel = "", - descriptionKey = "param.eqo.kway-t.maxPathLen") + descriptionKey = "param.eqo.kwayt.maxPathLen") public int maxPathLen; - @Option(names = "--eqo-kway-t-maxNumberOfSteps", + @Option(names = "--eqo-kwayt-maxNumberOfSteps", defaultValue = "100", paramLabel = "", - descriptionKey = "param.eqo.kway-t.maxNumberOfSteps") + descriptionKey = "param.eqo.kwayt.maxNumberOfSteps") public int maxNumberOfSteps; - @Option(names = "--eqo-kway-t-optimizationMetric", + @Option(names = "--eqo-kwayt-optimizationMetric", defaultValue = "STEPS", paramLabel = "", - descriptionKey = "param.eqo.kway-t.optimizationMetric") + descriptionKey = "param.eqo.kwayt.optimizationMetric") public OptimizationMetric optimizationMetric; - @Option(names = "--eqo-kway-t-generationMethod", + @Option(names = "--eqo-kwayt-generationMethod", defaultValue = "RANDOM", paramLabel = "", - descriptionKey = "param.eqo.kway-t.generationMethod") + descriptionKey = "param.eqo.kwayt.generationMethod") public GenerationMethod generationMethod; - @Option(names = "--eqo-kway-t-seed", + @Option(names = "--eqo-kwayt-seed", defaultValue = "42", paramLabel = "", - descriptionKey = "param.eqo.kway-t.seed") + descriptionKey = "param.eqo.kwayt.seed") public int seed; } @@ -149,55 +143,55 @@ public static class RandomMethod { public static class RandomWMethod { - @Option(names = "--eqo-random-w-minimalSize", + @Option(names = "--eqo-randomw-minimalSize", defaultValue = "0", paramLabel = "", - descriptionKey = "param.eqo.random-w.minimalSize") + descriptionKey = "param.eqo.randomw.minimalSize") public int minimalSize; - @Option(names = "--eqo-random-w-rndLength", + @Option(names = "--eqo-randomw-rndLength", defaultValue = "5", paramLabel = "", - descriptionKey = "param.eqo.random-w.rndLength") + descriptionKey = "param.eqo.randomw.rndLength") public int rndLength; - @Option(names = "--eqo-random-w-bound", + @Option(names = "--eqo-randomw-bound", defaultValue = "100", paramLabel = "", - descriptionKey = "param.eqo.random-w.bound") + descriptionKey = "param.eqo.randomw.bound") public int bound; - @Option(names = "--eqo-random-w-seed", + @Option(names = "--eqo-randomw-seed", defaultValue = "42", paramLabel = "", - descriptionKey = "param.eqo.random-w.seed") + descriptionKey = "param.eqo.randomw.seed") public int seed; } public static class RandomWpMethod { - @Option(names = "--eqo-random-wp-minimalSize", + @Option(names = "--eqo-randomwp-minimalSize", defaultValue = "0", paramLabel = "", - descriptionKey = "param.eqo.random-wp.minimalSize") + descriptionKey = "param.eqo.randomwp.minimalSize") public int minimalSize; - @Option(names = "--eqo-random-wp-rndLength", + @Option(names = "--eqo-randomwp-rndLength", defaultValue = "5", paramLabel = "", - descriptionKey = "param.eqo.random-wp.rndLength") + descriptionKey = "param.eqo.randomwp.rndLength") public int rndLength; - @Option(names = "--eqo-random-wp-bound", + @Option(names = "--eqo-randomwp-bound", defaultValue = "100", paramLabel = "", - descriptionKey = "param.eqo.random-wp.bound") + descriptionKey = "param.eqo.randomwp.bound") public int bound; - @Option(names = "--eqo-random-wp-seed", + @Option(names = "--eqo-randomwp-seed", defaultValue = "42", paramLabel = "", - descriptionKey = "param.eqo.random-wp.seed") + descriptionKey = "param.eqo.randomwp.seed") public int seed; } diff --git a/cli/src/main/resources/application.properties b/cli/src/main/resources/application.properties index e02ffdec6..0b45184b5 100644 --- a/cli/src/main/resources/application.properties +++ b/cli/src/main/resources/application.properties @@ -24,7 +24,7 @@ option.learner=The learning algorithm to use. Possible values are: ${COMPLETION- 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.snapshot=Directory for storing snapshots of the learner state 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. @@ -33,35 +33,35 @@ option.verbose=Increase verbosity. Each repetition increases the log level from 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.kways.combinationMethod=The 'combinationMethod' parameter of the 'KWayStateCoverEQOracle'. Possible values are: ${COMPLETION-CANDIDATES}. +param.eqo.kways.k=The 'k' parameter of the 'KWayStateCoverEQOracle'. +param.eqo.kways.randomWalkLen=The 'randomWalkLen' parameter of the 'KWayStateCoverEQOracle'. +param.eqo.kways.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.kwayt.generationMethod=The 'generationMethod' parameter of the 'KWayTransitionCoverEQOracle'. Possible values are: ${COMPLETION-CANDIDATES}. +param.eqo.kwayt.k=The 'k' parameter of the 'KWayTransitionCoverEQOracle'. +param.eqo.kwayt.maxNumberOfSteps=The 'maxNumberOfSteps' parameter of the 'KWayTransitionCoverEQOracle'. +param.eqo.kwayt.maxPathLen=The 'maxPathLen' parameter of the 'KWayTransitionCoverEQOracle'. +param.eqo.kwayt.numGeneratePaths=The 'numGeneratePaths' parameter of the 'KWayTransitionCoverEQOracle'. +param.eqo.kwayt.optimizationMetric=The 'optimizationMetric' parameter of the 'KWayTransitionCoverEQOracle'. Possible values are: ${COMPLETION-CANDIDATES}. +param.eqo.kwayt.randomWalkLen=The 'randomWalkLen' parameter of the 'KWayTransitionCoverEQOracle'. +param.eqo.kwayt.seed=The seed of the 'random' parameter of the 'KWayTransitionCoverEQOracle'. 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.randomw.bound=The 'bound' parameter of the 'RandomWMethodEQOracle'. +param.eqo.randomw.minimalSize=The 'minimalSize' parameter of the 'RandomWMethodEQOracle'. +param.eqo.randomw.rndLength=The 'rndLength' parameter of the 'RandomWMethodEQOracle'. +param.eqo.randomw.seed=The seed of the 'random' parameter of the 'RandomWMethodEQOracle'. + +param.eqo.randomwp.bound=The 'bound' parameter of the 'RandomWpMethodEQOracle'. +param.eqo.randomwp.minimalSize=The 'minimalSize' parameter of the 'RandomWpMethodEQOracle'. +param.eqo.randomwp.rndLength=The 'rndLength' parameter of the 'RandomWpMethodEQOracle'. +param.eqo.randomwp.seed=The seed of the 'random' parameter of the 'RandomWpMethodEQOracle'. + 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. diff --git a/cli/src/test/java/de/learnlib/cli/factory/EQOFactoryTest.java b/cli/src/test/java/de/learnlib/cli/factory/EQOFactoryTest.java index 983f63381..af0684f34 100644 --- a/cli/src/test/java/de/learnlib/cli/factory/EQOFactoryTest.java +++ b/cli/src/test/java/de/learnlib/cli/factory/EQOFactoryTest.java @@ -245,34 +245,34 @@ public void testRegularParameters() { 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-kways-combinationMethod=" + RandomUtil.choose(r, CombinationMethod.values())); + args.add("--eqo-kways-k=" + r.nextInt(randomBound)); + args.add("--eqo-kways-randomWalkLen=" + r.nextInt(randomBound)); + args.add("--eqo-kways-seed=" + r.nextInt(randomBound)); + + args.add("--eqo-kwayt-randomWalkLen=" + r.nextInt(randomBound)); + args.add("--eqo-kwayt-numGeneratePaths=" + r.nextInt(randomBound)); + args.add("--eqo-kwayt-maxPathLen=" + r.nextInt(randomBound)); + args.add("--eqo-kwayt-maxNumberOfSteps=" + r.nextInt(randomBound)); + args.add("--eqo-kwayt-k=" + r.nextInt(randomBound)); + args.add("--eqo-kwayt-optimizationMetric=" + RandomUtil.choose(r, OptimizationMetric.values())); + args.add("--eqo-kwayt-generationMethod=" + RandomUtil.choose(r, GenerationMethod.values())); + args.add("--eqo-kwayt-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-randomw-minimalSize=" + r.nextInt(randomBound)); + args.add("--eqo-randomw-rndLength=" + r.nextInt(randomBound)); + args.add("--eqo-randomw-bound=" + r.nextInt(randomBound)); + args.add("--eqo-randomw-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-randomwp-minimalSize=" + r.nextInt(randomBound)); + args.add("--eqo-randomwp-rndLength=" + r.nextInt(randomBound)); + args.add("--eqo-randomwp-bound=" + r.nextInt(randomBound)); + args.add("--eqo-randomwp-seed=" + r.nextInt(randomBound)); args.add("--eqo-w-lookahead=" + r.nextInt(randomBound)); args.add("--eqo-w-expectedSize=" + r.nextInt(randomBound)); From 1b466d3c2d7cfacb8f7194d72791c3417b2d7da8 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Fri, 31 Jul 2026 00:54:41 +0200 Subject: [PATCH 20/34] Revert "simplify --eqo-* param names" This reverts commit d209e962d51e27b960418112fb2d2f841da7e4c2. --- .../de/learnlib/cli/option/EQOParams.java | 82 ++++++++++--------- cli/src/main/resources/application.properties | 46 +++++------ .../learnlib/cli/factory/EQOFactoryTest.java | 42 +++++----- 3 files changed, 88 insertions(+), 82 deletions(-) diff --git a/cli/src/main/java/de/learnlib/cli/option/EQOParams.java b/cli/src/main/java/de/learnlib/cli/option/EQOParams.java index 6f422e9f0..f837e2db0 100644 --- a/cli/src/main/java/de/learnlib/cli/option/EQOParams.java +++ b/cli/src/main/java/de/learnlib/cli/option/EQOParams.java @@ -44,73 +44,79 @@ public class EQOParams { public static class KWayStateMethod { - @Option(names = "--eqo-kways-k", defaultValue = "2", paramLabel = "", descriptionKey = "param.eqo.kways.k") + @Option(names = "--eqo-kway-s-k", + defaultValue = "2", + paramLabel = "", + descriptionKey = "param.eqo.kway-s.k") public int k; - @Option(names = "--eqo-kways-randomWalkLen", + @Option(names = "--eqo-kway-s-randomWalkLen", defaultValue = "20", paramLabel = "", - descriptionKey = "param.eqo.kways.randomWalkLen") + descriptionKey = "param.eqo.kway-s.randomWalkLen") public int randomWalkLen; - @Option(names = "--eqo-kways-combinationMethod", + @Option(names = "--eqo-kway-s-combinationMethod", defaultValue = "PERMUTATIONS", paramLabel = "", - descriptionKey = "param.eqo.kways.combinationMethod") + descriptionKey = "param.eqo.kway-s.combinationMethod") public CombinationMethod combinationMethod; - @Option(names = "--eqo-kways-seed", + @Option(names = "--eqo-kway-s-seed", defaultValue = "42", paramLabel = "", - descriptionKey = "param.eqo.kways.seed") + descriptionKey = "param.eqo.kway-s.seed") public int seed; } public static class KWayTransitionMethod { - @Option(names = "--eqo-kwayt-k", defaultValue = "2", paramLabel = "", descriptionKey = "param.eqo.kwayt.k") + @Option(names = "--eqo-kway-t-k", + defaultValue = "2", + paramLabel = "", + descriptionKey = "param.eqo.kway-t.k") public int k; - @Option(names = "--eqo-kwayt-randomWalkLen", + @Option(names = "--eqo-kway-t-randomWalkLen", defaultValue = "10", paramLabel = "", - descriptionKey = "param.eqo.kwayt.randomWalkLen") + descriptionKey = "param.eqo.kway-t.randomWalkLen") public int randomWalkLen; - @Option(names = "--eqo-kwayt-numGeneratePaths", + @Option(names = "--eqo-kway-t-numGeneratePaths", defaultValue = "100", paramLabel = "", - descriptionKey = "param.eqo.kwayt.numGeneratePaths") + descriptionKey = "param.eqo.kway-t.numGeneratePaths") public int numGeneratePaths; - @Option(names = "--eqo-kwayt-maxPathLen", + @Option(names = "--eqo-kway-t-maxPathLen", defaultValue = "50", paramLabel = "", - descriptionKey = "param.eqo.kwayt.maxPathLen") + descriptionKey = "param.eqo.kway-t.maxPathLen") public int maxPathLen; - @Option(names = "--eqo-kwayt-maxNumberOfSteps", + @Option(names = "--eqo-kway-t-maxNumberOfSteps", defaultValue = "100", paramLabel = "", - descriptionKey = "param.eqo.kwayt.maxNumberOfSteps") + descriptionKey = "param.eqo.kway-t.maxNumberOfSteps") public int maxNumberOfSteps; - @Option(names = "--eqo-kwayt-optimizationMetric", + @Option(names = "--eqo-kway-t-optimizationMetric", defaultValue = "STEPS", paramLabel = "", - descriptionKey = "param.eqo.kwayt.optimizationMetric") + descriptionKey = "param.eqo.kway-t.optimizationMetric") public OptimizationMetric optimizationMetric; - @Option(names = "--eqo-kwayt-generationMethod", + @Option(names = "--eqo-kway-t-generationMethod", defaultValue = "RANDOM", paramLabel = "", - descriptionKey = "param.eqo.kwayt.generationMethod") + descriptionKey = "param.eqo.kway-t.generationMethod") public GenerationMethod generationMethod; - @Option(names = "--eqo-kwayt-seed", + @Option(names = "--eqo-kway-t-seed", defaultValue = "42", paramLabel = "", - descriptionKey = "param.eqo.kwayt.seed") + descriptionKey = "param.eqo.kway-t.seed") public int seed; } @@ -143,55 +149,55 @@ public static class RandomMethod { public static class RandomWMethod { - @Option(names = "--eqo-randomw-minimalSize", + @Option(names = "--eqo-random-w-minimalSize", defaultValue = "0", paramLabel = "", - descriptionKey = "param.eqo.randomw.minimalSize") + descriptionKey = "param.eqo.random-w.minimalSize") public int minimalSize; - @Option(names = "--eqo-randomw-rndLength", + @Option(names = "--eqo-random-w-rndLength", defaultValue = "5", paramLabel = "", - descriptionKey = "param.eqo.randomw.rndLength") + descriptionKey = "param.eqo.random-w.rndLength") public int rndLength; - @Option(names = "--eqo-randomw-bound", + @Option(names = "--eqo-random-w-bound", defaultValue = "100", paramLabel = "", - descriptionKey = "param.eqo.randomw.bound") + descriptionKey = "param.eqo.random-w.bound") public int bound; - @Option(names = "--eqo-randomw-seed", + @Option(names = "--eqo-random-w-seed", defaultValue = "42", paramLabel = "", - descriptionKey = "param.eqo.randomw.seed") + descriptionKey = "param.eqo.random-w.seed") public int seed; } public static class RandomWpMethod { - @Option(names = "--eqo-randomwp-minimalSize", + @Option(names = "--eqo-random-wp-minimalSize", defaultValue = "0", paramLabel = "", - descriptionKey = "param.eqo.randomwp.minimalSize") + descriptionKey = "param.eqo.random-wp.minimalSize") public int minimalSize; - @Option(names = "--eqo-randomwp-rndLength", + @Option(names = "--eqo-random-wp-rndLength", defaultValue = "5", paramLabel = "", - descriptionKey = "param.eqo.randomwp.rndLength") + descriptionKey = "param.eqo.random-wp.rndLength") public int rndLength; - @Option(names = "--eqo-randomwp-bound", + @Option(names = "--eqo-random-wp-bound", defaultValue = "100", paramLabel = "", - descriptionKey = "param.eqo.randomwp.bound") + descriptionKey = "param.eqo.random-wp.bound") public int bound; - @Option(names = "--eqo-randomwp-seed", + @Option(names = "--eqo-random-wp-seed", defaultValue = "42", paramLabel = "", - descriptionKey = "param.eqo.randomwp.seed") + descriptionKey = "param.eqo.random-wp.seed") public int seed; } diff --git a/cli/src/main/resources/application.properties b/cli/src/main/resources/application.properties index 0b45184b5..e02ffdec6 100644 --- a/cli/src/main/resources/application.properties +++ b/cli/src/main/resources/application.properties @@ -24,7 +24,7 @@ option.learner=The learning algorithm to use. Possible values are: ${COMPLETION- 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 state 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.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. @@ -33,35 +33,35 @@ option.verbose=Increase verbosity. Each repetition increases the log level from param.eqo.heading=%nParameters for equivalence oracles:%n -param.eqo.kways.combinationMethod=The 'combinationMethod' parameter of the 'KWayStateCoverEQOracle'. Possible values are: ${COMPLETION-CANDIDATES}. -param.eqo.kways.k=The 'k' parameter of the 'KWayStateCoverEQOracle'. -param.eqo.kways.randomWalkLen=The 'randomWalkLen' parameter of the 'KWayStateCoverEQOracle'. -param.eqo.kways.seed=The seed of the 'random' parameter of the 'KWayStateCoverEQOracle'. +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.kwayt.generationMethod=The 'generationMethod' parameter of the 'KWayTransitionCoverEQOracle'. Possible values are: ${COMPLETION-CANDIDATES}. -param.eqo.kwayt.k=The 'k' parameter of the 'KWayTransitionCoverEQOracle'. -param.eqo.kwayt.maxNumberOfSteps=The 'maxNumberOfSteps' parameter of the 'KWayTransitionCoverEQOracle'. -param.eqo.kwayt.maxPathLen=The 'maxPathLen' parameter of the 'KWayTransitionCoverEQOracle'. -param.eqo.kwayt.numGeneratePaths=The 'numGeneratePaths' parameter of the 'KWayTransitionCoverEQOracle'. -param.eqo.kwayt.optimizationMetric=The 'optimizationMetric' parameter of the 'KWayTransitionCoverEQOracle'. Possible values are: ${COMPLETION-CANDIDATES}. -param.eqo.kwayt.randomWalkLen=The 'randomWalkLen' parameter of the 'KWayTransitionCoverEQOracle'. -param.eqo.kwayt.seed=The seed of the 'random' parameter of the 'KWayTransitionCoverEQOracle'. +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.randomw.bound=The 'bound' parameter of the 'RandomWMethodEQOracle'. -param.eqo.randomw.minimalSize=The 'minimalSize' parameter of the 'RandomWMethodEQOracle'. -param.eqo.randomw.rndLength=The 'rndLength' parameter of the 'RandomWMethodEQOracle'. -param.eqo.randomw.seed=The seed of the 'random' parameter of the 'RandomWMethodEQOracle'. - -param.eqo.randomwp.bound=The 'bound' parameter of the 'RandomWpMethodEQOracle'. -param.eqo.randomwp.minimalSize=The 'minimalSize' parameter of the 'RandomWpMethodEQOracle'. -param.eqo.randomwp.rndLength=The 'rndLength' parameter of the 'RandomWpMethodEQOracle'. -param.eqo.randomwp.seed=The seed of the 'random' parameter of the 'RandomWpMethodEQOracle'. - 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. diff --git a/cli/src/test/java/de/learnlib/cli/factory/EQOFactoryTest.java b/cli/src/test/java/de/learnlib/cli/factory/EQOFactoryTest.java index af0684f34..983f63381 100644 --- a/cli/src/test/java/de/learnlib/cli/factory/EQOFactoryTest.java +++ b/cli/src/test/java/de/learnlib/cli/factory/EQOFactoryTest.java @@ -245,34 +245,34 @@ public void testRegularParameters() { args.add("-sa"); Arrays.stream(EQOracle.values()).map(EQOracle::name).forEach(n -> args.add("-e" + n)); - args.add("--eqo-kways-combinationMethod=" + RandomUtil.choose(r, CombinationMethod.values())); - args.add("--eqo-kways-k=" + r.nextInt(randomBound)); - args.add("--eqo-kways-randomWalkLen=" + r.nextInt(randomBound)); - args.add("--eqo-kways-seed=" + r.nextInt(randomBound)); - - args.add("--eqo-kwayt-randomWalkLen=" + r.nextInt(randomBound)); - args.add("--eqo-kwayt-numGeneratePaths=" + r.nextInt(randomBound)); - args.add("--eqo-kwayt-maxPathLen=" + r.nextInt(randomBound)); - args.add("--eqo-kwayt-maxNumberOfSteps=" + r.nextInt(randomBound)); - args.add("--eqo-kwayt-k=" + r.nextInt(randomBound)); - args.add("--eqo-kwayt-optimizationMetric=" + RandomUtil.choose(r, OptimizationMetric.values())); - args.add("--eqo-kwayt-generationMethod=" + RandomUtil.choose(r, GenerationMethod.values())); - args.add("--eqo-kwayt-seed=" + r.nextInt(randomBound)); + 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-randomw-minimalSize=" + r.nextInt(randomBound)); - args.add("--eqo-randomw-rndLength=" + r.nextInt(randomBound)); - args.add("--eqo-randomw-bound=" + r.nextInt(randomBound)); - args.add("--eqo-randomw-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-randomwp-minimalSize=" + r.nextInt(randomBound)); - args.add("--eqo-randomwp-rndLength=" + r.nextInt(randomBound)); - args.add("--eqo-randomwp-bound=" + r.nextInt(randomBound)); - args.add("--eqo-randomwp-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)); From 15b604864bbf38e0b20c80e72bcca2665745d2ad Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Fri, 31 Jul 2026 01:15:25 +0200 Subject: [PATCH 21/34] add MalerPnueli + RivestSchapire learner and adjust learner names --- .../cli/adapter/ProceduralDFAAdapter.java | 16 + .../cli/adapter/ProceduralMealyAdapter.java | 17 ++ .../learnlib/cli/factory/LearnerFactory.java | 35 ++- .../java/de/learnlib/cli/option/Learner.java | 16 +- .../java/de/learnlib/cli/option/Type.java | 2 +- .../java/de/learnlib/cli/ApplicationIT.java | 19 +- .../cli/factory/LearnerFactoryTest.java | 283 +++++++++++------- 7 files changed, 251 insertions(+), 137 deletions(-) diff --git a/cli/src/main/java/de/learnlib/cli/adapter/ProceduralDFAAdapter.java b/cli/src/main/java/de/learnlib/cli/adapter/ProceduralDFAAdapter.java index 0823ca1b5..11eb41287 100644 --- a/cli/src/main/java/de/learnlib/cli/adapter/ProceduralDFAAdapter.java +++ b/cli/src/main/java/de/learnlib/cli/adapter/ProceduralDFAAdapter.java @@ -21,7 +21,9 @@ 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; @@ -51,6 +53,13 @@ public ExtensibleLStarDFAAdapter(Alphabet alphabet, MembershipOracle 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) { @@ -58,6 +67,13 @@ public OPLearnerDFAAdapter(Alphabet alphabet, MembershipOracle or } } + 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) { diff --git a/cli/src/main/java/de/learnlib/cli/adapter/ProceduralMealyAdapter.java b/cli/src/main/java/de/learnlib/cli/adapter/ProceduralMealyAdapter.java index 2b9c91464..056951949 100644 --- a/cli/src/main/java/de/learnlib/cli/adapter/ProceduralMealyAdapter.java +++ b/cli/src/main/java/de/learnlib/cli/adapter/ProceduralMealyAdapter.java @@ -22,7 +22,9 @@ 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; @@ -63,6 +65,13 @@ public ExtensibleLStarMealyAdapter(Alphabet alphabet, MembershipOracle 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) { @@ -77,6 +86,14 @@ public OPLearnerMealyAdapter(Alphabet alphabet, MembershipOracle> } } + 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) { diff --git a/cli/src/main/java/de/learnlib/cli/factory/LearnerFactory.java b/cli/src/main/java/de/learnlib/cli/factory/LearnerFactory.java index 6be351a79..2dbc10368 100644 --- a/cli/src/main/java/de/learnlib/cli/factory/LearnerFactory.java +++ b/cli/src/main/java/de/learnlib/cli/factory/LearnerFactory.java @@ -27,14 +27,18 @@ 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; @@ -63,12 +67,14 @@ private LearnerFactory() { public static DFAConstructor, I> getDFALearner(Options options) { return switch (options.learner) { - case KV -> KearnsVaziraniDFAAdapter::new; - case LLAMBDA -> LLambdaDFAAdapter::new; - case LSTAR -> ExtensibleLStarDFAAdapter::new; - case OP -> OPLearnerDFAAdapter::new; + 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 TTTLAMBDA -> TTTLambdaDFAAdapter::new; + case TTT_LAMBDA -> TTTLambdaDFAAdapter::new; default -> throw new UnsupportedCombinationException(options); }; } @@ -76,13 +82,15 @@ public static DFAConstructor, I> getDFALearner(Options options) public static MealyConstructor, I, O> getMealyLearner(Options options) { return switch (options.learner) { case DHC -> MealyDHCAdapter::new; - case KV -> KearnsVaziraniMealyAdapter::new; - case LLAMBDA -> LLambdaMealyAdapter::new; - case LSTAR -> ExtensibleLStarMealyAdapter::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 OP -> OPLearnerMealyAdapter::new; case TTT -> TTTLearnerMealyAdapter::new; - case TTTLAMBDA -> TTTLambdaMealyAdapter::new; + case TTT_LAMBDA -> TTTLambdaMealyAdapter::new; default -> throw new UnsupportedCombinationException(options); }; } @@ -90,13 +98,13 @@ public static MealyConstructor, I, O> getMealyLearner(Options public static AdaptiveConstructor, MealyMachine, I, O> getAdaptiveLearner(Options options) { return switch (options.learner) { case ADT -> ADTLearner::new; - case LSHARP -> LSharpMealy::new; + case L_SHARP -> LSharpMealy::new; default -> throw new UnsupportedCombinationException(options); }; } public static PresetConstructor, NFA, I, Boolean> getNFALearner(Options options) { - if (options.learner == Learner.NLSTAR) { + if (options.learner == Learner.NL_STAR) { return NLStarLearner::new; } throw new UnsupportedCombinationException(options); @@ -120,7 +128,8 @@ public static PresetConstructor, SPA, I, Bo public static PresetConstructor, OneSEVPA, I, Boolean> getVPALearner(Options options) { return switch (options.learner) { - case OP -> (alphabet, mqo) -> new OPLearnerVPABuilder().withAlphabet(alphabet).withOracle(mqo).create(); + 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); diff --git a/cli/src/main/java/de/learnlib/cli/option/Learner.java b/cli/src/main/java/de/learnlib/cli/option/Learner.java index ebf2ff340..da5e5d109 100644 --- a/cli/src/main/java/de/learnlib/cli/option/Learner.java +++ b/cli/src/main/java/de/learnlib/cli/option/Learner.java @@ -19,13 +19,15 @@ public enum Learner { ADT, DHC, - KV, - LLAMBDA, - LSHARP, - LSTAR, - NLSTAR, - OP, + KEARNS_VAZIRANI, + L_LAMBDA, + L_SHARP, + L_STAR, + NL_STAR, + MALER_PNUELI, + OBSERVATION_PACK, + RIVEST_SCHAPIRE, SPARSE, TTT, - TTTLAMBDA, + TTT_LAMBDA, } diff --git a/cli/src/main/java/de/learnlib/cli/option/Type.java b/cli/src/main/java/de/learnlib/cli/option/Type.java index d2cb1ddd6..a8e0b9cf2 100644 --- a/cli/src/main/java/de/learnlib/cli/option/Type.java +++ b/cli/src/main/java/de/learnlib/cli/option/Type.java @@ -38,7 +38,7 @@ public Runner runner(Options options) { MEALY { @Override public Runner runner(Options options) { - if (options.learner == Learner.ADT || options.learner == Learner.LSHARP) { + if (options.learner == Learner.ADT || options.learner == Learner.L_SHARP) { return new AdaptiveRunner<>(AlphabetFactory::getRegularAlphabet, MQOFactory::getAdaptiveOracle, LearnerFactory::getAdaptiveLearner, diff --git a/cli/src/test/java/de/learnlib/cli/ApplicationIT.java b/cli/src/test/java/de/learnlib/cli/ApplicationIT.java index 905f9b274..57b55b236 100644 --- a/cli/src/test/java/de/learnlib/cli/ApplicationIT.java +++ b/cli/src/test/java/de/learnlib/cli/ApplicationIT.java @@ -85,7 +85,6 @@ public void testVerbosity() { 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); @@ -96,19 +95,19 @@ public void testVerbosity() { @Test public void testStatelessDFA() throws IOException { - checkRegularExecution(new String[] {"-lLSTAR", "-eSAMPLE", "--eqo-sample=a b", STATELESS}, "/ser/dfa.dot"); + checkRegularExecution(new String[] {"-lL_STAR", "-eSAMPLE", "--eqo-sample=a b", STATELESS}, "/ser/dfa.dot"); } @Test public void testMealyPreset() throws IOException { - checkRegularExecution(new String[] {"-lLSTAR", "-tMEALY", "-eSAMPLE", "--eqo-sample=a b", STATELESS}, + checkRegularExecution(new String[] {"-lL_STAR", "-tMEALY", "-eSAMPLE", "--eqo-sample=a b", STATELESS}, "/ser/mealy.dot"); } @Test public void testMealyAdaptive() throws IOException { checkRegularExecution(new String[] {"-tMEALY", - "-lLSHARP", + "-lL_SHARP", "-eSAMPLE", "--eqo-sample=a b", "--reset=reset", @@ -117,25 +116,25 @@ public void testMealyAdaptive() throws IOException { @Test public void testStatelessNFA() throws IOException { - checkRegularExecution(new String[] {"-tNFA", "-lNLSTAR", "-eSAMPLE", "--eqo-sample=a b", STATELESS}, + checkRegularExecution(new String[] {"-tNFA", "-lNL_STAR", "-eSAMPLE", "--eqo-sample=a b", STATELESS}, "/ser/dfa.dot"); } @Test public void testStatelessSBA() throws IOException { - checkProceduralExecution(new String[] {"-lLSTAR", "-tSBA", "-eSAMPLE", "--eqo-sample=S a R", STATELESS_SBA}, + checkProceduralExecution(new String[] {"-lL_STAR", "-tSBA", "-eSAMPLE", "--eqo-sample=S a R", STATELESS_SBA}, null); } @Test public void testStatelessSPA() throws IOException { - checkProceduralExecution(new String[] {"-lLSTAR", "-tSPA", "-eSAMPLE", "--eqo-sample=S a R", STATELESS_SPA}, + checkProceduralExecution(new String[] {"-lL_STAR", "-tSPA", "-eSAMPLE", "--eqo-sample=S a R", STATELESS_SPA}, "/ser/spa.dot"); } @Test public void testStatelessSPMM() throws IOException { - checkProceduralExecution(new String[] {"-lLSTAR", + checkProceduralExecution(new String[] {"-lL_STAR", "-tSPMM", "-eSAMPLE", "--eqo-sample=S a R", @@ -150,7 +149,7 @@ public void testStatelessVPA() throws IOException { @Test public void testStatistics() throws IOException { - checkRegularExecution(new String[] {"-lLSTAR", "-eSAMPLE", "--eqo-sample=abab", "--stats", STATELESS}, null); + checkRegularExecution(new String[] {"-lL_STAR", "-eSAMPLE", "--eqo-sample=abab", "--stats", STATELESS}, null); StatisticsService statistics = Statistics.getService(); Assert.assertTrue(statistics.getCount(CounterOracle.KEY_SYMBOL.withId(AbstractRunner.EQO_KEY)).isPresent()); @@ -161,7 +160,7 @@ public void testStatistics() throws IOException { @Test public void testAdaptiveStatistics() throws IOException { checkRegularExecution(new String[] {"-tMEALY", - "-lLSHARP", + "-lL_SHARP", "-eSAMPLE", "--eqo-sample=abab", "--reset=reset", diff --git a/cli/src/test/java/de/learnlib/cli/factory/LearnerFactoryTest.java b/cli/src/test/java/de/learnlib/cli/factory/LearnerFactoryTest.java index d5ccf45ca..2a4f69ea5 100644 --- a/cli/src/test/java/de/learnlib/cli/factory/LearnerFactoryTest.java +++ b/cli/src/test/java/de/learnlib/cli/factory/LearnerFactoryTest.java @@ -32,6 +32,8 @@ 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; @@ -39,6 +41,8 @@ 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; @@ -91,22 +95,30 @@ private static Object[][] dfaConfigs() { 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 KV -> - new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-lKV"}, KearnsVaziraniDFA.class}; - case LLAMBDA -> - new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-lLLAMBDA"}, LLambdaDFA.class}; - case LSHARP -> - new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-lLSHARP"}, Exception.class}; - case LSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-lLSTAR"}, - ExtensibleLStarDFA.class}; - case NLSTAR -> - new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-lNLSTAR"}, Exception.class}; - case OP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-lOP"}, OPLearnerDFA.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 TTTLAMBDA -> - new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-lTTTLAMBDA"}, TTTLambdaDFA.class}; + case TTT_LAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-lTTT_LAMBDA"}, + TTTLambdaDFA.class}; }; } @@ -138,24 +150,33 @@ private static Object[][] mealyConfigs() { Exception.class}; case DHC -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lDHC"}, MealyDHC.class}; - case KV -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lKV"}, - KearnsVaziraniMealy.class}; - case LLAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lLLAMBDA"}, - LLambdaMealy.class}; - case LSHARP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lLSHARP"}, - Exception.class}; - case LSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lLSTAR"}, - ExtensibleLStarMealy.class}; - case NLSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lNLSTAR"}, - Exception.class}; - case OP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lOP"}, - OPLearnerMealy.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 TTTLAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lTTTLAMBDA"}, - TTTLambdaMealy.class}; + case TTT_LAMBDA -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lTTT_LAMBDA"}, + TTTLambdaMealy.class}; }; } @@ -188,24 +209,33 @@ private static Object[][] adaptiveConfigs() { ADTLearner.class}; case DHC -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lDHC"}, Exception.class}; - case KV -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lKV"}, - Exception.class}; - case LLAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lLLAMBDA"}, - Exception.class}; - case LSHARP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lLSHARP"}, - LSharpMealy.class}; - case LSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lLSTAR"}, - Exception.class}; - case NLSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lNLSTAR"}, + 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 OP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lOP"}, - 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 TTTLAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lTTTLAMBDA"}, - Exception.class}; + case TTT_LAMBDA -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tMEALY", "-lTTT_LAMBDA"}, + Exception.class}; }; } @@ -238,24 +268,32 @@ private static Object[][] nfaConfigs() { 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 KV -> - new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lKV"}, Exception.class}; - case LLAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lLLAMBDA"}, + 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 LSHARP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lLSHARP"}, + case L_STAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lL_STAR"}, Exception.class}; - case LSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lLSTAR"}, - Exception.class}; - case NLSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lNLSTAR"}, - NLStarLearner.class}; - case OP -> - new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lOP"}, 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 TTTLAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lTTTLAMBDA"}, - Exception.class}; + case TTT_LAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tNFA", "-lTTT_LAMBDA"}, + Exception.class}; }; } @@ -288,24 +326,32 @@ private static Object[][] sbaConfigs() { 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 KV -> - new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSBA", "-lKV"}, SBALearner.class}; - case LLAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSBA", "-lLLAMBDA"}, - SBALearner.class}; - case LSHARP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSBA", "-lLSHARP"}, - Exception.class}; - case LSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSBA", "-lLSTAR"}, - SBALearner.class}; - case NLSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSBA", "-lNLSTAR"}, - Exception.class}; - case OP -> - new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSBA", "-lOP"}, SBALearner.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 TTTLAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSBA", "-lTTTLAMBDA"}, - SBALearner.class}; + case TTT_LAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSBA", "-lTTT_LAMBDA"}, + SBALearner.class}; }; } @@ -340,24 +386,32 @@ private static Object[][] spaConfigs() { 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 KV -> - new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPA", "-lKV"}, SPALearner.class}; - case LLAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPA", "-lLLAMBDA"}, - SPALearner.class}; - case LSHARP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPA", "-lLSHARP"}, - Exception.class}; - case LSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPA", "-lLSTAR"}, - SPALearner.class}; - case NLSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPA", "-lNLSTAR"}, - Exception.class}; - case OP -> - new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPA", "-lOP"}, SPALearner.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 TTTLAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPA", "-lTTTLAMBDA"}, - SPALearner.class}; + case TTT_LAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPA", "-lTTT_LAMBDA"}, + SPALearner.class}; }; } @@ -392,24 +446,33 @@ private static Object[][] spmmConfigs() { Exception.class}; case DHC -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lDHC"}, SPMMLearner.class}; - case KV -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lKV"}, - SPMMLearner.class}; - case LLAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lLLAMBDA"}, - SPMMLearner.class}; - case LSHARP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lLSHARP"}, - Exception.class}; - case LSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lLSTAR"}, - SPMMLearner.class}; - case NLSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lNLSTAR"}, - Exception.class}; - case OP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lOP"}, - 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 TTTLAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lTTTLAMBDA"}, - SPMMLearner.class}; + case TTT_LAMBDA -> + new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tSPMM", "-lTTT_LAMBDA"}, + SPMMLearner.class}; }; } @@ -445,24 +508,32 @@ private static Object[][] vpaConfigs() { 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 KV -> - new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lKV"}, Exception.class}; - case LLAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lLLAMBDA"}, + 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 LSHARP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lLSHARP"}, + case L_STAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lL_STAR"}, Exception.class}; - case LSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lLSTAR"}, - Exception.class}; - case NLSTAR -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lNLSTAR"}, - Exception.class}; - case OP -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lOP"}, - OPLearnerVPA.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 TTTLAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lTTTLAMBDA"}, - Exception.class}; + case TTT_LAMBDA -> new Object[] {new String[] {ApplicationIT.STATELESS, "-sa", "-tVPA", "-lTTT_LAMBDA"}, + Exception.class}; }; } From b590ce11f655d3e4075bf29f2a7c41458810324c Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Fri, 31 Jul 2026 03:06:05 +0200 Subject: [PATCH 22/34] include license information in jlink artifact --- cli/pom.xml | 33 +++++++++++++++++++++++++++++++++ pom.xml | 6 ++++++ 2 files changed, 39 insertions(+) diff --git a/cli/pom.xml b/cli/pom.xml index 570ded687..96d6929a5 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -230,6 +230,28 @@ limitations under the License. cli + + 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 @@ -241,6 +263,17 @@ limitations under the License. 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 diff --git a/pom.xml b/pom.xml index cf4be6687..a3f8e3cf4 100644 --- a/pom.xml +++ b/pom.xml @@ -257,6 +257,7 @@ limitations under the License. 3.5.0 3.12.0 3.3.0 + 2.7.1 1.0.0 1.7.1 3.28.0 @@ -976,6 +977,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 From 7689b39e2c556db5054a41fb3b5a0c63c962a031 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Fri, 31 Jul 2026 15:53:27 +0200 Subject: [PATCH 23/34] cleanup checkerframework config --- build-parent/pom.xml | 8 +------- cli/pom.xml | 9 ++++----- .../de/learnlib/cli/util/SnapshottingExperiment.java | 5 ++++- cli/src/main/java/module-info.java | 2 ++ cli/src/main/resources/logback.xml | 2 +- pom.xml | 1 - 6 files changed, 12 insertions(+), 15 deletions(-) diff --git a/build-parent/pom.xml b/build-parent/pom.xml index 1db74d04b..b6b4851db 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -263,13 +263,7 @@ limitations under the License. true true - - + ${project.build.directory}/test-checkerframework org.checkerframework diff --git a/cli/pom.xml b/cli/pom.xml index 96d6929a5..3e1610d71 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -178,17 +178,16 @@ limitations under the License. org.apache.fory fory-core + + org.checkerframework + checker-qual + org.slf4j slf4j-api - - org.checkerframework - checker-qual - test - org.mockito mockito-core diff --git a/cli/src/main/java/de/learnlib/cli/util/SnapshottingExperiment.java b/cli/src/main/java/de/learnlib/cli/util/SnapshottingExperiment.java index f1ff0f2c0..ff2bbb0a9 100644 --- a/cli/src/main/java/de/learnlib/cli/util/SnapshottingExperiment.java +++ b/cli/src/main/java/de/learnlib/cli/util/SnapshottingExperiment.java @@ -30,8 +30,9 @@ import net.automatalib.automaton.concept.FiniteRepresentation; import net.automatalib.serialization.InputModelSerializer; import org.apache.fory.Fory; -import org.apache.fory.annotation.Nullable; import org.apache.fory.exception.ForyException; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; final class SnapshottingExperiment extends Experiment { @@ -93,6 +94,7 @@ protected void initializeLearning() { } } + @RequiresNonNull("this.resumeFrom") private void resumLearner(Resumable resumable) throws IOException { @SuppressWarnings("unchecked") final T state = (T) FORY.deserialize(Files.readAllBytes(resumeFrom)); @@ -108,6 +110,7 @@ protected void postRefinementHook() { } } + @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 diff --git a/cli/src/main/java/module-info.java b/cli/src/main/java/module-info.java index 3509f3660..f4671d634 100644 --- a/cli/src/main/java/module-info.java +++ b/cli/src/main/java/module-info.java @@ -54,4 +54,6 @@ requires org.apache.fory.core; requires org.slf4j; + requires static org.checkerframework.checker.qual; + } diff --git a/cli/src/main/resources/logback.xml b/cli/src/main/resources/logback.xml index 2aceeb7fc..18c501741 100644 --- a/cli/src/main/resources/logback.xml +++ b/cli/src/main/resources/logback.xml @@ -25,4 +25,4 @@ limitations under the License. - \ No newline at end of file + diff --git a/pom.xml b/pom.xml index a3f8e3cf4..afb7384c5 100644 --- a/pom.xml +++ b/pom.xml @@ -942,7 +942,6 @@ limitations under the License. maven-compiler-plugin ${compiler-plugin.version} - -Xlint:unchecked de.learnlib.tooling From 5c294907a2bf8c1d97d36564f3a01147d1dc436f Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Tue, 4 Aug 2026 14:24:58 +0200 Subject: [PATCH 24/34] add rudimentary class validation during deserialization + cleanups --- cli/pom.xml | 6 +++--- .../cli/util/SnapshottingExperiment.java | 10 ++++++++++ cli/src/test/resources/logback-test.xml | 2 +- .../de/learnlib/testsupport/ResumeUtils.java | 18 +++++++++++++----- 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/cli/pom.xml b/cli/pom.xml index 3e1610d71..1eb33ee9d 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -29,9 +29,9 @@ limitations under the License. LearnLib :: CLI - A standalone application to offer the functionality of LearnLib via a command-line interface. - *Note:* This artifact is not intended as a library and therefore not deployed to Maven Central but instead as a - direct download (e.g., from GitHub). + 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). diff --git a/cli/src/main/java/de/learnlib/cli/util/SnapshottingExperiment.java b/cli/src/main/java/de/learnlib/cli/util/SnapshottingExperiment.java index ff2bbb0a9..bdf89f115 100644 --- a/cli/src/main/java/de/learnlib/cli/util/SnapshottingExperiment.java +++ b/cli/src/main/java/de/learnlib/cli/util/SnapshottingExperiment.java @@ -31,6 +31,9 @@ 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; @@ -40,12 +43,19 @@ final class SnapshottingExperiment extends 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-"); } diff --git a/cli/src/test/resources/logback-test.xml b/cli/src/test/resources/logback-test.xml index 6611f17a0..90b5f4a99 100644 --- a/cli/src/test/resources/logback-test.xml +++ b/cli/src/test/resources/logback-test.xml @@ -25,4 +25,4 @@ limitations under the License. - \ No newline at end of file + 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 a7955ca58..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 @@ -18,22 +18,30 @@ import de.learnlib.Resumable; 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 Fory FURY; + private static final Fory FORY; static { - LoggerFactory.useSlf4jLogging(true); - FURY = Fory.builder() + // 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); } private ResumeUtils() { @@ -41,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); } } From 608d866c9940d7e2549269976c03615777855182 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Mon, 10 Aug 2026 12:59:54 +0200 Subject: [PATCH 25/34] add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b72e2d87..f6c827492 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 commandline 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. From e61eb83adde32fedc104e391bbb99119226e349e Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Tue, 11 Aug 2026 12:54:00 +0200 Subject: [PATCH 26/34] add missing --add-opens + test --- cli/pom.xml | 26 +++++++ .../java/de/learnlib/cli/CheckBinary.java | 71 +++++++++++++++++++ pom.xml | 17 ++--- 3 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 cli/src/test/java/de/learnlib/cli/CheckBinary.java diff --git a/cli/pom.xml b/cli/pom.xml index 1eb33ee9d..37c244c78 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -229,6 +229,29 @@ limitations under the License. cli + + maven-failsafe-plugin + + + check-binary + + integration-test + verify + + + + ${project.build.directory}/maven-jlink/default + + + **/CheckBinary.java + + + **/*IT.java + + + + + org.codehaus.mojo license-maven-plugin @@ -273,6 +296,9 @@ limitations under the License. legal/third-party/licenses + + --add-opens=java.base/java.lang.invoke=org.apache.fory.core + true true true 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..1d7470076 --- /dev/null +++ b/cli/src/test/java/de/learnlib/cli/CheckBinary.java @@ -0,0 +1,71 @@ +/* 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.nio.file.Path; +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 = Path.of(System.getProperty("learnlib.binary.path", "")) + .resolve("bin") + .resolve("learnlib") + .toAbsolutePath() + .toFile(); + 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/pom.xml b/pom.xml index afb7384c5..96b1a73c2 100644 --- a/pom.xml +++ b/pom.xml @@ -803,14 +803,6 @@ limitations under the License. true - - - - integration-test - verify - - - org.jacoco @@ -1061,6 +1053,15 @@ limitations under the License. org.apache.maven.plugins maven-failsafe-plugin + + + default + + integration-test + verify + + + From c9c5d530d9102d9d4aa08cc17d8cf88e0dd9b2d3 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Tue, 11 Aug 2026 12:56:14 +0200 Subject: [PATCH 27/34] cleanup --- build-parent/pom.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build-parent/pom.xml b/build-parent/pom.xml index b6b4851db..c805cf3eb 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -80,6 +80,8 @@ 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/DFAExample.class de/learnlib/util/MealyExample.class de/learnlib/util/MooreExample.class From 10a8a724b8ae8375cb9e3468fcf3e24a189052f0 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Tue, 11 Aug 2026 12:57:22 +0200 Subject: [PATCH 28/34] wording --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6c827492..a4ec2b907 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +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 commandline interface without the need for Java or Maven. +* 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. From 8f7a67566647ed63b0a180d6e30c84b98830da34 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Tue, 11 Aug 2026 15:11:55 +0200 Subject: [PATCH 29/34] handle python availability more gracefully --- .../de/learnlib/cli/AbstractPythonTest.java | 39 +++++++++++++++++++ .../java/de/learnlib/cli/ApplicationIT.java | 15 +------ .../learnlib/cli/factory/MQOFactoryTest.java | 3 +- cli/src/test/resources/sul/sba.py | 2 +- cli/src/test/resources/sul/spa.py | 2 +- cli/src/test/resources/sul/stateful.py | 2 +- cli/src/test/resources/sul/stateless.py | 2 +- .../test/resources/sul/stateless_broken.py | 2 +- cli/src/test/resources/sul/stateless_large.py | 2 +- .../oracle/membership/AbstractPythonTest.java | 8 ++-- .../src/test/resources/stateful_stdin_sul.py | 2 +- .../src/test/resources/stateful_sul.py | 2 +- .../src/test/resources/stateless_stdin_sul.py | 2 +- .../src/test/resources/stateless_sul.py | 2 +- 14 files changed, 56 insertions(+), 29 deletions(-) create mode 100644 cli/src/test/java/de/learnlib/cli/AbstractPythonTest.java 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..30c76d4fd --- /dev/null +++ b/cli/src/test/java/de/learnlib/cli/AbstractPythonTest.java @@ -0,0 +1,39 @@ +/* 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 net.automatalib.common.util.process.ProcessUtil; +import org.testng.SkipException; +import org.testng.annotations.BeforeClass; + +public abstract class AbstractPythonTest { + + protected static final String PROGRAM = "python3"; + + @BeforeClass + public void setUp() { + try { + if (ProcessUtil.invokeProcess(new String[] {PROGRAM, "--version"}) != 0) { + throw new SkipException("python3 not supported"); + } + } catch (IOException | InterruptedException e) { + 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 index 57b55b236..8742f9d88 100644 --- a/cli/src/test/java/de/learnlib/cli/ApplicationIT.java +++ b/cli/src/test/java/de/learnlib/cli/ApplicationIT.java @@ -34,16 +34,14 @@ import de.learnlib.statistic.Statistics; import de.learnlib.statistic.StatisticsService; import net.automatalib.common.util.IOUtil; -import net.automatalib.common.util.process.ProcessUtil; import org.checkerframework.checker.nullness.qual.Nullable; import org.slf4j.LoggerFactory; import org.testng.Assert; import org.testng.SkipException; -import org.testng.annotations.BeforeTest; import org.testng.annotations.Test; import picocli.CommandLine; -public class ApplicationIT { +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"); @@ -61,17 +59,6 @@ private static String getPathToScript(String script) { } } - @BeforeTest - public void setUp() { - try { - if (ProcessUtil.invokeProcess(new String[] {"python", "--version"}) != 0) { - throw new SkipException("python not supported"); - } - } catch (IOException | InterruptedException e) { - throw new SkipException("python not supported"); - } - } - @Test public void testVerbosity() { final Logger root = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); diff --git a/cli/src/test/java/de/learnlib/cli/factory/MQOFactoryTest.java b/cli/src/test/java/de/learnlib/cli/factory/MQOFactoryTest.java index 3ef6effab..f40e7fec9 100644 --- a/cli/src/test/java/de/learnlib/cli/factory/MQOFactoryTest.java +++ b/cli/src/test/java/de/learnlib/cli/factory/MQOFactoryTest.java @@ -23,6 +23,7 @@ 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; @@ -52,7 +53,7 @@ import org.testng.Assert; import org.testng.annotations.Test; -public class MQOFactoryTest { +public class MQOFactoryTest extends AbstractPythonTest { private final File sulFile; diff --git a/cli/src/test/resources/sul/sba.py b/cli/src/test/resources/sul/sba.py index 2d7f96767..c6e0e8a57 100755 --- a/cli/src/test/resources/sul/sba.py +++ b/cli/src/test/resources/sul/sba.py @@ -1,4 +1,4 @@ -#!/bin/python +#!/usr/bin/env python3 import sys def main(): diff --git a/cli/src/test/resources/sul/spa.py b/cli/src/test/resources/sul/spa.py index 856b8805e..9c13c9433 100755 --- a/cli/src/test/resources/sul/spa.py +++ b/cli/src/test/resources/sul/spa.py @@ -1,4 +1,4 @@ -#!/bin/python +#!/usr/bin/env python3 import sys def main(): diff --git a/cli/src/test/resources/sul/stateful.py b/cli/src/test/resources/sul/stateful.py index 5544ac2fb..db13e295f 100755 --- a/cli/src/test/resources/sul/stateful.py +++ b/cli/src/test/resources/sul/stateful.py @@ -1,4 +1,4 @@ -#!/bin/python +#!/usr/bin/env python3 from pathlib import Path import sys diff --git a/cli/src/test/resources/sul/stateless.py b/cli/src/test/resources/sul/stateless.py index eaaa6a480..9e25a3824 100755 --- a/cli/src/test/resources/sul/stateless.py +++ b/cli/src/test/resources/sul/stateless.py @@ -1,4 +1,4 @@ -#!/bin/python +#!/usr/bin/env python3 import sys def main(): diff --git a/cli/src/test/resources/sul/stateless_broken.py b/cli/src/test/resources/sul/stateless_broken.py index d3106c5ef..7b0f6e583 100755 --- a/cli/src/test/resources/sul/stateless_broken.py +++ b/cli/src/test/resources/sul/stateless_broken.py @@ -1,4 +1,4 @@ -#!/bin/python +#!/usr/bin/env python3 import sys def main(): diff --git a/cli/src/test/resources/sul/stateless_large.py b/cli/src/test/resources/sul/stateless_large.py index 2fe05c50d..1672d688f 100755 --- a/cli/src/test/resources/sul/stateless_large.py +++ b/cli/src/test/resources/sul/stateless_large.py @@ -1,4 +1,4 @@ -#!/bin/python +#!/usr/bin/env python3 import sys def main(): 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/resources/stateful_stdin_sul.py b/oracles/membership-oracles/src/test/resources/stateful_stdin_sul.py index 29be60747..8d926547e 100755 --- a/oracles/membership-oracles/src/test/resources/stateful_stdin_sul.py +++ b/oracles/membership-oracles/src/test/resources/stateful_stdin_sul.py @@ -1,4 +1,4 @@ -#!/bin/python +#!/usr/bin/env python3 from pathlib import Path import sys diff --git a/oracles/membership-oracles/src/test/resources/stateful_sul.py b/oracles/membership-oracles/src/test/resources/stateful_sul.py index 1280e951f..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 diff --git a/oracles/membership-oracles/src/test/resources/stateless_stdin_sul.py b/oracles/membership-oracles/src/test/resources/stateless_stdin_sul.py index 10dd54763..3cb1381d2 100755 --- a/oracles/membership-oracles/src/test/resources/stateless_stdin_sul.py +++ b/oracles/membership-oracles/src/test/resources/stateless_stdin_sul.py @@ -1,4 +1,4 @@ -#!/bin/python +#!/usr/bin/env python3 import sys def main(): diff --git a/oracles/membership-oracles/src/test/resources/stateless_sul.py b/oracles/membership-oracles/src/test/resources/stateless_sul.py index bc576afbb..31b54ccb9 100755 --- a/oracles/membership-oracles/src/test/resources/stateless_sul.py +++ b/oracles/membership-oracles/src/test/resources/stateless_sul.py @@ -1,4 +1,4 @@ -#!/bin/python +#!/usr/bin/env python3 import sys def main(): From 3a68b0388ee3fa303b9ab5a3895476ae75cf28cb Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Tue, 11 Aug 2026 15:44:02 +0200 Subject: [PATCH 30/34] conditionally set --add-opens option --- cli/pom.xml | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/cli/pom.xml b/cli/pom.xml index 37c244c78..570c48941 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -296,9 +296,6 @@ limitations under the License. legal/third-party/licenses - - --add-opens=java.base/java.lang.invoke=org.apache.fory.core - true true true @@ -312,5 +309,28 @@ limitations under the License. + + jlink-on-java-24 + + [24,] + + + + + + org.apache.maven.plugins + maven-jlink-plugin + + + + + --add-opens=java.base/java.lang.invoke=org.apache.fory.core + + + + + + + From 47d126996735855044c22f1e8bf5abc3c1aa0138 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Tue, 11 Aug 2026 17:01:12 +0200 Subject: [PATCH 31/34] cli: invoke script via python interpreter as on Windows the scripts alone are not detected as executable applications --- .../de/learnlib/cli/AbstractPythonTest.java | 33 ++++++-- .../java/de/learnlib/cli/ApplicationIT.java | 78 ++++++++++++++----- .../test/java/de/learnlib/cli/SnapshotIT.java | 44 ++++++++--- .../learnlib/cli/factory/MQOFactoryTest.java | 3 +- 4 files changed, 122 insertions(+), 36 deletions(-) diff --git a/cli/src/test/java/de/learnlib/cli/AbstractPythonTest.java b/cli/src/test/java/de/learnlib/cli/AbstractPythonTest.java index 30c76d4fd..2041dcc58 100644 --- a/cli/src/test/java/de/learnlib/cli/AbstractPythonTest.java +++ b/cli/src/test/java/de/learnlib/cli/AbstractPythonTest.java @@ -15,7 +15,10 @@ */ 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; @@ -23,15 +26,33 @@ public abstract class AbstractPythonTest { - protected static final String PROGRAM = "python3"; + 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() { - try { - if (ProcessUtil.invokeProcess(new String[] {PROGRAM, "--version"}) != 0) { - throw new SkipException("python3 not supported"); - } - } catch (IOException | InterruptedException e) { + 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 index 8742f9d88..f331a9841 100644 --- a/cli/src/test/java/de/learnlib/cli/ApplicationIT.java +++ b/cli/src/test/java/de/learnlib/cli/ApplicationIT.java @@ -82,61 +82,97 @@ public void testVerbosity() { @Test public void testStatelessDFA() throws IOException { - checkRegularExecution(new String[] {"-lL_STAR", "-eSAMPLE", "--eqo-sample=a b", STATELESS}, "/ser/dfa.dot"); + 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[] {"-lL_STAR", "-tMEALY", "-eSAMPLE", "--eqo-sample=a b", STATELESS}, - "/ser/mealy.dot"); + 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[] {"-tMEALY", + checkRegularExecution(new String[] {PROGRAM, + "--args", + STATELESS, + "-tMEALY", "-lL_SHARP", "-eSAMPLE", "--eqo-sample=a b", - "--reset=reset", - STATEFUL}, "/ser/mealy.dot"); + "--reset=reset"}, "/ser/mealy.dot"); } @Test public void testStatelessNFA() throws IOException { - checkRegularExecution(new String[] {"-tNFA", "-lNL_STAR", "-eSAMPLE", "--eqo-sample=a b", STATELESS}, - "/ser/dfa.dot"); + 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[] {"-lL_STAR", "-tSBA", "-eSAMPLE", "--eqo-sample=S a R", STATELESS_SBA}, - null); + 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[] {"-lL_STAR", "-tSPA", "-eSAMPLE", "--eqo-sample=S a R", STATELESS_SPA}, - "/ser/spa.dot"); + 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[] {"-lL_STAR", + checkProceduralExecution(new String[] {PROGRAM, + "--args", + STATELESS_SBA, + "-lL_STAR", "-tSPMM", "-eSAMPLE", "--eqo-sample=S a R", - "-d=\\s", - STATELESS_SBA}, "/ser/spmm.dot"); + "-d=\\s"}, "/ser/spmm.dot"); } @Test public void testStatelessVPA() throws IOException { - checkProceduralExecution(new String[] {"-tVPA", "-eSAMPLE", "--eqo-sample=S a R", STATELESS_SPA}, null); + checkProceduralExecution(new String[] {PROGRAM, + "--args", + STATELESS_SPA, + "-tVPA", + "-eSAMPLE", + "--eqo-sample=S a R"}, null); } @Test public void testStatistics() throws IOException { - checkRegularExecution(new String[] {"-lL_STAR", "-eSAMPLE", "--eqo-sample=abab", "--stats", STATELESS}, null); + 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()); @@ -146,13 +182,15 @@ public void testStatistics() throws IOException { @Test public void testAdaptiveStatistics() throws IOException { - checkRegularExecution(new String[] {"-tMEALY", + checkRegularExecution(new String[] {PROGRAM, + "--args", + STATEFUL, + "-tMEALY", "-lL_SHARP", "-eSAMPLE", "--eqo-sample=abab", "--reset=reset", - "--stats", - STATEFUL}, null); + "--stats"}, null); StatisticsService statistics = Statistics.getService(); Assert.assertTrue(statistics.getCount(CounterAdaptiveQueryOracle.KEY_SYMBOL.withId(AbstractRunner.EQO_KEY)) diff --git a/cli/src/test/java/de/learnlib/cli/SnapshotIT.java b/cli/src/test/java/de/learnlib/cli/SnapshotIT.java index faf9a3956..c09486d3b 100644 --- a/cli/src/test/java/de/learnlib/cli/SnapshotIT.java +++ b/cli/src/test/java/de/learnlib/cli/SnapshotIT.java @@ -60,7 +60,9 @@ public void testNonResumableLearner() throws IOException { snapshot.deleteOnExit(); int exitCode; - exitCode = cmd.execute(ApplicationIT.STATELESS, + exitCode = cmd.execute(ApplicationIT.PROGRAM, + "--args", + ApplicationIT.STATELESS, "-sa", "-sb", "-tMEALY", @@ -69,7 +71,9 @@ public void testNonResumableLearner() throws IOException { resume.toString()); Assert.assertTrue(exitCode > 0); - exitCode = cmd.execute(ApplicationIT.STATELESS, + exitCode = cmd.execute(ApplicationIT.PROGRAM, + "--args", + ApplicationIT.STATELESS, "-sa", "-sb", "-tMEALY", @@ -78,7 +82,9 @@ public void testNonResumableLearner() throws IOException { snapshot.toString()); Assert.assertTrue(exitCode > 0); - exitCode = cmd.execute(ApplicationIT.STATELESS, + exitCode = cmd.execute(ApplicationIT.PROGRAM, + "--args", + ApplicationIT.STATELESS, "-sa", "-sb", "-tMEALY", @@ -100,13 +106,27 @@ public void testInvalidSnapshotProperties() throws IOException { int exitCode; - exitCode = cmd.execute(ApplicationIT.STATELESS, "-sa", "-sb", "--resume-from", resume.toString()); + exitCode = cmd.execute(ApplicationIT.PROGRAM, + "--args", + ApplicationIT.STATELESS, + "-sa", + "-sb", + "--resume-from", + resume.toString()); Assert.assertTrue(exitCode > 0); - exitCode = cmd.execute(ApplicationIT.STATELESS, "-sa", "-sb", "--snapshot-dir", snapshot.toString()); + exitCode = cmd.execute(ApplicationIT.PROGRAM, + "--args", + ApplicationIT.STATELESS, + "-sa", + "-sb", + "--snapshot-dir", + snapshot.toString()); Assert.assertTrue(exitCode > 0); - exitCode = cmd.execute(ApplicationIT.STATELESS, + exitCode = cmd.execute(ApplicationIT.PROGRAM, + "--args", + ApplicationIT.STATELESS, "-sa", "-sb", "--resume-from", @@ -126,7 +146,9 @@ public void testSuspendResume() throws IOException { int exitCode; // run regular scenario with snapshotting - exitCode = cmd.execute(ApplicationIT.STATELESS_LARGE, + exitCode = cmd.execute(ApplicationIT.PROGRAM, + "--args", + ApplicationIT.STATELESS_LARGE, "-sa", "-sb", "-tMEALY", @@ -148,7 +170,9 @@ public void testSuspendResume() throws IOException { // 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.STATELESS_BROKEN, + exitCode = cmd2.execute(ApplicationIT.PROGRAM, + "--args", + ApplicationIT.STATELESS_BROKEN, "-sa", "-sb", "-tMEALY", @@ -168,7 +192,9 @@ public void testRegularExecution() throws IOException { output.deleteOnExit(); // run regular scenario with snapshotting - final int exitCode = cmd.execute(ApplicationIT.STATELESS_LARGE, + final int exitCode = cmd.execute(ApplicationIT.PROGRAM, + "--args", + ApplicationIT.STATELESS_LARGE, "-sa", "-sb", "-tMEALY", diff --git a/cli/src/test/java/de/learnlib/cli/factory/MQOFactoryTest.java b/cli/src/test/java/de/learnlib/cli/factory/MQOFactoryTest.java index f40e7fec9..9af4acd6e 100644 --- a/cli/src/test/java/de/learnlib/cli/factory/MQOFactoryTest.java +++ b/cli/src/test/java/de/learnlib/cli/factory/MQOFactoryTest.java @@ -315,10 +315,11 @@ public void testAdaptiveOracle() { @Test public void testOutputTransformer() { - File sulFile = new File(ApplicationIT.STATELESS_BROKEN); + 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); From 22417fe6f02254ebca461338b8d3d7e71c3848a2 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Tue, 11 Aug 2026 19:22:31 +0200 Subject: [PATCH 32/34] sett full (and platform-specific) path in jlink IT --- cli/pom.xml | 17 ++++++++++++++++- .../test/java/de/learnlib/cli/CheckBinary.java | 8 ++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/cli/pom.xml b/cli/pom.xml index 570c48941..7f98a309d 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -34,6 +34,10 @@ limitations under the License. GitHub). + + ${project.build.directory}/maven-jlink/default/bin/learnlib + + @@ -240,7 +244,7 @@ limitations under the License. - ${project.build.directory}/maven-jlink/default + ${learnlib.binary.path} **/CheckBinary.java @@ -332,5 +336,16 @@ limitations under the License. + + windows + + + windows + + + + ${project.build.directory}/maven-jlink/default/bin/learnlib.bat + + diff --git a/cli/src/test/java/de/learnlib/cli/CheckBinary.java b/cli/src/test/java/de/learnlib/cli/CheckBinary.java index 1d7470076..9bb2c3ce2 100644 --- a/cli/src/test/java/de/learnlib/cli/CheckBinary.java +++ b/cli/src/test/java/de/learnlib/cli/CheckBinary.java @@ -21,7 +21,6 @@ import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.nio.file.Path; import java.util.Arrays; import java.util.List; @@ -39,11 +38,8 @@ public class CheckBinary { @Test public void testInvokeBinary() throws IOException, InterruptedException { - final File bin = Path.of(System.getProperty("learnlib.binary.path", "")) - .resolve("bin") - .resolve("learnlib") - .toAbsolutePath() - .toFile(); + 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(); From 64d7544be6e03d6180fc6dfb6e2e0535ba47ae57 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Tue, 11 Aug 2026 19:22:52 +0200 Subject: [PATCH 33/34] wording --- cli/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/pom.xml b/cli/pom.xml index 7f98a309d..4d233dd2c 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -31,7 +31,7 @@ limitations under the License. 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). + GitHub releases). From 35e16c033469600674e853de48169ec67db9b800 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Tue, 11 Aug 2026 19:46:42 +0200 Subject: [PATCH 34/34] coveralls: s/Example/Experiment/ --- build-parent/pom.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build-parent/pom.xml b/build-parent/pom.xml index c805cf3eb..b6f18f236 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -81,10 +81,10 @@ limitations under the License. de/learnlib/oracle/property/DFA*Chain.class de/learnlib/oracle/property/Mealy*Chain.class - - de/learnlib/util/DFAExample.class - de/learnlib/util/MealyExample.class - de/learnlib/util/MooreExample.class + + de/learnlib/util/DFAExperiment.class + de/learnlib/util/MealyExperiment.class + de/learnlib/util/MooreExperiment.class