Attention: This version of LearnLib (JLearn) is out of date and no longer maintained. The current version can be found on Gitub.com.
LearnLib. a framework for automata learning

API tutorial

This page contains a simple tutorial on how to connect a learning algorithm to a real system under test (SUT). As an example we will learn the behavior of a Java class. This class Pool implements a pool of objects. The pool is implemented around a set. Thus, each object can only be contained once. The pool has methods to

The source of the implementation looks like:

public class Pool {

    private Set<Object> pool = new HashSet<Object>();

    public void add(Object o) {
        pool.add(o);
    }

    public void remove(Object o) {
        pool.remove(o);
    }

    public boolean isEmpty() {
        return pool.isEmpty();
    }    
}

Connecting to the system

Learning algorithms can not be connected directly to real systems. Learning algorithms use alphabets. A learning algorithm will formulate a test case that is to be executed on the system in an abstract language (as words that are build from the input alphabet). It will also expect the reaction of the system to be encoded into words over the output alphabet.

To connect a learning algorithm and a real system, we will need to have a test-driver, which translates input words into sequences of real actions (e.g., method calls) and real outputs (e.g., return values) into output words. In the test-driver, we first define these alphabets.

public class PoolTestDriver {
    // input symbols
    private static final Symbol O1_IN = new SymbolImpl("o1in");
    private static final Symbol O2_IN = new SymbolImpl("o2in");
    private static final Symbol O1_OUT = new SymbolImpl("o1out");
    private static final Symbol O2_OUT = new SymbolImpl("o2out");
    private static final Symbol ISEMPTY = new SymbolImpl("isempty?");

    // input alphabet used by learning algorithm
    public static final Alphabet SIGMA;
    static
    {
        SIGMA = new AlphabetImpl();
        SIGMA.addSymbol(O1_IN);
        SIGMA.addSymbol(O2_IN);
        SIGMA.addSymbol(O1_OUT);
        SIGMA.addSymbol(O2_OUT);
        SIGMA.addSymbol(ISEMPTY);
    }

    // return values
    private static final Symbol TOP = new SymbolImpl("true");
    private static final Symbol BOT = new SymbolImpl("false");
    private static final Symbol NOOP = new SymbolImpl("-");

    ...

Here, single actions are of type Symbol (which is a Java Interface). We here use SymbolImpl, a lightweight implementation of Symbol. Each Symbol holds a user-defined object. In this tutorial we use plain strings as user objects. Symbols are compared (and identified) using the equals() method of the user object. We define one Symbol for each action we plan to invoke on the SUT and for each answer we expect from the SUT. The input symbols will be given to the learning algorithm in form of an Alphabet. Again, we use the basic implementation AlphabetImpl, that is provided by LearnLib. The learning algorithms work with a flexible output alphabet. We therefore can omit building an explicit output alphabet. In this example we will learn the behavior of the pool for two objects that can be added or removed.

After defining the side of the test-driver that will be exposed to the learning algorithm later, we need some code to instrument the SUT. This code essentially consists of two parts:

The learning algorithm will need to have a means of executing words of inputs on the system and get back words of outputs. In the test-driver we therefor a method that allows the execution of single symbols on the SUT. The method simply translates input symbols into method calls and the results of method calls into output symbols.

The learning algorithm will further require a means of reseting the SUT into its initial state. We provide this by simply creating a new instance of the SUT each time a reset is required.

    ...
    // system under test
    public Pool pool;

    // local test variables
    private final String o1 = "o1";
    private final String o2 = "o2";

    public Symbol executeSymbol(Symbol s) {
        if (s.equals(O1_IN)) {
            pool.add(o1);
            return NOOP;
        } else if (s.equals(O2_IN)) {
            pool.add(o2);
            return NOOP;
        } else if (s.equals(O1_OUT)) {
            pool.remove(o1);
            return NOOP;
        } else if (s.equals(O2_OUT)) {
            pool.remove(o2);
            return NOOP;
        } else if (s.equals(ISEMPTY)){
            return pool.isEmpty() ? TOP : BOT;
        }
        return null;
    }

    public void reset() {
        this.pool = new Pool();
    }
}

Oracles

The test-driver still contains very SUT-specific code. To make explicit the different concerns when connecting to a real system we split the implementation of the test-driver and the Oracle. A learning algorithm will expect an instance of Oracle as connection to the system. As the concrete realization of an oracle typically depends on the concrete SUT to be learned, one will not find a general basic implementation of an Oracle in LearnLib.

public class PoolOracle implements Oracle {

    private PoolTestDriver testDriver;

    public PoolOracle() {        
        testDriver = new PoolTestDriver();
    }

    @Override
    public Word processQuery(Word query) throws LearningException {
    ...

An oracle has to implement only one method: Word processQuery(Word query). In this example we implement this method to only

    ...
    public Word processQuery(Word query) throws LearningException {
        Word trace = new WordImpl();
        testDriver.reset();
        for (Symbol i : query.getSymbolList()) {
            Symbol o = testDriver.executeSymbol(i);
            trace.addSymbol(o);
        }
        return trace;
    }
}

Learning the behavior of the Pool

We now have everything at hand to learn the behavior of our SUT.

Logging

LearnLib provides a logging framework that allows you to log output to different facilities. For each LoggingAppender that is added to the logging system you can assign a threshold level. Only messages above this level will be passed to the according appender. We here use a PrintStreamLoggingAppender to log to System.out and second appender that logs to a html file.

LearnLog.addAppender(new PrintStreamLoggingAppender(LogLevel.INFO,System.out));
LearnLog.addAppender(new HtmlLoggingAppender(LogLevel.DEBUG,
                "/tmp/learn.html", false, false, false));

Setup

Now, let us create the oracle, the learning algorithm, and the equivalence algorithm. We here use a W-Method conformance test to simulate equivalence queries and initialize it with N=4 (where N is an upper bound on number of states the SUT can have). In this example we chose this bound to be tight - which for real black-boxes normally one cannot! LearnLib comprises a number of algorithms for equivalence approximation.

As learning algorithm we chose the ObservationPack algorithm (there are several other learning alorithms implemented in LearnLib). To both the learning algorithm and the equivalence algorithm we pass the oracle. To the learning algorithm we also pass the input alphabet. The equivalence algorithm will later construct the input alphabet on-the-fly from the hypothesis.

// create oracle for mutex
Oracle testOracle = new PoolOracle();

EquivalenceOracle eqtest = new WMethodEquivalenceTest(4);
eqtest.setOracle(testOracle);

Learner learner = new ObservationPack();
learner.setOracle(testOracle);
learner.setAlphabet(PoolTestDriver.SIGMA);

Main loop

In the main loop the actual learning round are carried out. Invoking the learn() method on a learning algorithm will make the algorithm complete the actual round of learning. After completion, the current hypothesis is passed to the equivalence algorithm. In case a counterexample is found, this will be passed back to the learning algorithm. If no counterexample is found, the main loop will be left and the learned model will be persisted into a file the dot-utility of Graphviz can process using the DotUtil class.

boolean equiv = false;
while (!equiv)
{
      // learn one round
      learner.learn();
      Automaton hyp = learner.getResult();

      logger.log(LogLevel.INFO, "hypothetical states: " +
                    hyp.getAllStates().size());

      // search for counterexample
      EquivalenceOracleOutput o = eqtest.findCounterExample(hyp);
      if (o == null) {
            equiv = true;
            continue;
      }
      learner.addCounterExample(o.getCounterExample(), o.getOracleOutput());
}

DotUtil.writeDot(learner.getResult(), new File("/tmp/learnresult.dot"));

Running the example

Below you see the output the PrintStreamLoggingAppender will produce on System.out for this example. It is splitted into several parts. Furthermore you can see what the hypothesis and data structure of the algorithm look like at the end of every round.

Round 1

========================================================================
[INFO] Closing Table
========================================================================
------------------------------------------------------------------------
[INFO]Splitting Component

suffix: [isempty? ]
New access sequence: [o1in ]
New trace: [false ]
Old accesss sequence: []
Old trace: [true ]

------------------------------------------------------------------------
[INFO] New state: o1in 
========================================================================
[INFO] Checking Consistency
========================================================================
[INFO] hypothetical states: 2


Round 2

========================================================================
[INFO] Equivalence Query
========================================================================
------------------------------------------------------------------------
[INFO]W-method conformance-test setup

hypothesis size (n): 2
maximal size (N): 4
alphabet size (|A|): 5
prefixtree size (|p|): 11
suffix set size (|z|): 1
max. add. distance (w): 2
test size: SUM ( |p| * (|A|^i) * |z| ), for all 1 <= i <= w
test size: 330
------------------------------------------------------------------------
========================================================================
[INFO] Analyzing Counterexample
========================================================================
========================================================================
[INFO] Closing Table
========================================================================
------------------------------------------------------------------------
[INFO]Splitting Component

suffix: [o1out isempty? ]
New access sequence: [o2in ]
New trace: [false ]
Old accesss sequence: [o1in ]
Old trace: [true ]

------------------------------------------------------------------------
[INFO] New state: o2in 
========================================================================
[INFO] Checking Consistency
========================================================================
========================================================================
[INFO] Analyzing Counterexample
========================================================================
[INFO] [o2in o1out isempty? ] is not a counterexample
[INFO] hypothetical states: 3


Round 3

========================================================================
[INFO] Equivalence Query
========================================================================
------------------------------------------------------------------------
[INFO]W-method conformance-test setup

hypothesis size (n): 3
maximal size (N): 4
alphabet size (|A|): 5
prefixtree size (|p|): 16
suffix set size (|z|): 2
max. add. distance (w): 1
test size: SUM ( |p| * (|A|^i) * |z| ), for all 1 <= i <= w
test size: 160
------------------------------------------------------------------------
========================================================================
[INFO] Analyzing Counterexample
========================================================================
========================================================================
[INFO] Closing Table
========================================================================
------------------------------------------------------------------------
[INFO]Splitting Component

suffix: [o2out isempty? ]
New access sequence: [o1in o2in ]
New trace: [false ]
Old accesss sequence: [o2in ]
Old trace: [true ]

------------------------------------------------------------------------
[INFO] New state: o1in o2in 
========================================================================
[INFO] Checking Consistency
========================================================================
========================================================================
[INFO] Analyzing Counterexample
========================================================================
[INFO] [o2in o1in o2out isempty? ] is not a counterexample
[INFO] hypothetical states: 4


Last Equivalence Query

The last W-Method test contains 0 tests as the hypothesis has already 4 states (which we set as an upper bound to the number of states in the real system).

========================================================================
[INFO] Equivalence Query
========================================================================
------------------------------------------------------------------------
[INFO]W-method conformance-test setup

hypothesis size (n): 4
maximal size (N): 4
alphabet size (|A|): 5
prefixtree size (|p|): 21
suffix set size (|z|): 3
max. add. distance (w): 0
test size: SUM ( |p| * (|A|^i) * |z| ), for all 1 <= i <= w
test size: 0
------------------------------------------------------------------------
[INFO] DONE!!