packages feed

haal-0.7.0.0: test/AutomatonSpec.hs

{-# LANGUAGE ScopedTypeVariables #-}

-- | This module tests the Mealy automaton implementation.
module AutomatonSpec (
    spec,
)
where

import Control.Monad (replicateM)
import Control.Monad.Identity (runIdentity)
import qualified Data.List as List
import qualified Data.Map as Map
import qualified Data.Maybe as Maybe
import qualified Data.Set as Set
import Haal.Automaton.MealyAutomaton (
    MealyAutomaton (..),
    mealyDelta,
    mealyLambda,
    mkMealyAutomaton,
 )
import Haal.BlackBox
import Test.Hspec (Spec, context, describe, it, shouldBe)
import Test.QuickCheck (Property, forAll, property, (.&&.), (===), (==>))
import Utils (Input (..), Mealy (..), NonMinimalMealy (..), Output (..), genState, statesAreEquivalent)

-- The global characterizing set of a non minimal mealy automaton contains
-- the empty list. This will fail if 'stateSpace' has fewer than 6-7 states
-- because a lot of test cases will be discarded.
prop_emptyListInCharacterizingSet :: NonMinimalMealy -> Int -> Int -> Property
prop_emptyListInCharacterizingSet (NonMinimalMealy automaton) s1 s2 =
    statesAreEquivalent automaton s1 s2
        && s1
            /= s2
        ==> []
            `Set.member` globalCharacterizingSet automaton

-- Two states that are not equivalent can be distinguished.
prop_existsDistinguishingSequence :: Mealy Input Output -> Int -> Int -> Property
prop_existsDistinguishingSequence (Mealy automaton) s1 s2 =
    not (statesAreEquivalent automaton s1 s2) ==>
        output1
            /= output2
            && output1 /= []
            && output2 /= []
  where
    dist = distinguish automaton s1 s2
    (_, output1) = runIdentity $ walk (update automaton s1) dist
    (_, output2) = runIdentity $ walk (update automaton s2) dist

-- The map returned by 'mealyTransitions' is equivalent to the 'mealyLambda'
-- and 'mealyDelta' functions of the automaton.
prop_mappingEquivalentToFunctions :: Mealy Input Output -> Bool
prop_mappingEquivalentToFunctions (Mealy automaton) =
    let transs = transitions automaton
        alphabet = Set.toList $ inputs automaton
        sts = Set.toList $ states automaton
        -- Calculate outputs using mealyDelta and mealyLambda
        mapOutputs =
            [ Maybe.fromJust (Map.lookup (s, a) transs)
            | s <- sts
            , a <- alphabet
            ]
        funOutputs = [(mealyDelta automaton s a, mealyLambda automaton s a) | s <- sts, a <- alphabet]
     in mapOutputs == funOutputs

-- The access sequences returned by 'mealyAccessSequences' cover all reachable states.
prop_completeAccessSequences :: Mealy Input Output -> Property
prop_completeAccessSequences (Mealy automaton) = sts == rsts ==> allin
  where
    seqs = accessSequences automaton
    sts = states automaton
    rsts = reachable automaton
    allin = all (`Map.member` seqs) rsts

-- The access sequences returned by 'mealyAccessSequences' are the shortest
prop_shortestAccessSequences :: Mealy Input Output -> Int -> Int -> Property
prop_shortestAccessSequences (Mealy automaton) s1 s2 =
    s1 `Set.member` rsts
        && s2 `Set.member` rsts
        && existsS1toS2
        ==> List.length seq2 <= List.length seq1 + 1
  where
    rsts = reachable automaton
    transs = transitions automaton
    accessSeqs = accessSequences automaton
    seq1 = accessSeqs Map.! s1
    seq2 = accessSeqs Map.! s2
    -- find transition in map (s, i) -> (s, o)
    -- that leads from s1 to s2
    listed = Map.toList transs
    filtering (s, i) = s == s1 && fst (transs Map.! (s, i)) == s2
    maybeTransition = List.find filtering $ List.map fst listed
    existsS1toS2 = case maybeTransition of
        Nothing -> False
        Just _ -> True

{- | A counter modulo 5 that outputs 'Y' when input 'A' makes it wrap around and
'X' otherwise; every other input resets it.
-}
counter :: MealyAutomaton Int Input Output
counter = mkMealyAutomaton delta lambda (Set.fromList [0 .. 4]) 0
  where
    delta s A = (s + 1) `mod` 5
    delta _ _ = 0
    lambda 4 A = Y
    lambda _ _ = X

-- | 'counter' with its states renumbered, so a different but equivalent automaton.
renumberedCounter :: MealyAutomaton Int Input Output
renumberedCounter = mkMealyAutomaton delta lambda (Set.fromList [10 .. 14]) 10
  where
    delta s A = 10 + (s - 10 + 1) `mod` 5
    delta _ _ = 10
    lambda 14 A = Y
    lambda _ _ = X

-- | A single state that always outputs 'X'.
oneState :: MealyAutomaton Int Input Output
oneState = mkMealyAutomaton (\_ _ -> 0) (\_ _ -> X) (Set.fromList [0]) 0

-- | The outputs of an automaton on a word, from its initial state.
run :: MealyAutomaton Int Input Output -> [Input] -> [Output]
run aut = snd . walkPure (resetPure aut)

{- | The word 'difference' returns is a shortest witness: the two automata
differ on its last output, and agree on every word one symbol shorter. Outputs
are prefix-closed, so they then agree on every shorter word too. Witnesses of
more than 5 symbols are only checked for the first part, to keep the
enumeration small.
-}
prop_differenceIsShortestWitness :: Mealy Input Output -> Mealy Input Output -> Property
prop_differenceIsShortestWitness (Mealy a) (Mealy b) = case difference a b of
    Nothing -> property True
    Just w ->
        let (oa, ob) = (run a w, run b w)
            shorter = replicateM (length w - 1) [minBound .. maxBound]
            agreeOnShorter = length w > 5 || all (\v -> run a v == run b v) shorter
         in (last oa /= last ob) === True .&&. agreeOnShorter === True

-- | An automaton has no difference with itself.
prop_noDifferenceWithItself :: Mealy Input Output -> Property
prop_noDifferenceWithItself (Mealy a) = difference a a === Nothing

spec :: Spec
spec = do
    describe "BlackBox.difference" $ do
        it "finds the shortest word on which the counter and a one-state automaton differ" $ do
            let w = difference counter oneState
            w `shouldBe` Just [A, A, A, A, A]
            fmap (run counter) w `shouldBe` Just [X, X, X, X, Y]
        it "finds no difference between an automaton and itself" $
            difference counter counter `shouldBe` Nothing
        it "finds no difference between equivalent automata with different states" $
            difference counter renumberedCounter `shouldBe` Nothing
        it "returns a shortest witness" $
            property prop_differenceIsShortestWitness
        it "returns nothing for an automaton compared with itself" $
            property prop_noDifferenceWithItself

    describe "Blackbox.distinguish for MealyAutomaton" $
        context "if 2 automatons states are not equivalent" $
            it "returns an input sequence that distinguishes them" $
                property $ \aut ->
                    forAll genState $ \s1 ->
                        forAll genState $ \s2 ->
                            prop_existsDistinguishingSequence aut s1 s2

    describe "BlackBox.globalCharacterizingSet for MealyAutomaton" $
        context "if the automaton contains at least 2 equivalent states" $
            it "returns a set that contains the empty list" $
                property $ \aut ->
                    forAll genState $ \s1 ->
                        forAll genState $ \s2 ->
                            prop_emptyListInCharacterizingSet aut s1 s2

    describe "MealyAutomaton.mealyTransitions" $
        it "returns a map equivalent to the transition and output functions of the model" $
            property
                prop_mappingEquivalentToFunctions

    describe "BlackBox.accessSequences for MealyAutomaton" $ do
        it "returns a map from states to list of inputs that covers all reachable states" $
            property
                prop_completeAccessSequences

        it "returns a map from reachable states to shortest list of inputs that access them" $
            property $ \aut ->
                forAll genState $ \s1 ->
                    forAll genState $ \s2 ->
                        prop_shortestAccessSequences aut s1 s2