packages feed

haal-0.7.0.0: test/DotSpec.hs

-- | This module tests the DOT parser and the table encoding used by haal-gen.
module DotSpec (
    spec,
)
where

import Data.Char (chr, ord)
import Data.Either (isLeft)
import Data.List (isInfixOf)
import qualified Data.Map as Map
import qualified Data.Set as Set
import Haal.Automaton.MealyAutomaton (
    MealyAutomaton,
    mealyTransitions,
    mkMealyAutomatonTable,
 )
import Haal.BlackBox (initial, states)
import Haal.Dot
import Test.Hspec (Spec, describe, it, shouldBe, shouldSatisfy)
import Test.QuickCheck (Property, counterexample, property, (===))
import Utils (Input, Mealy (..), Output)

-- A small automaton whose states and symbols appear out of alphabetical order.
smallDot :: String
smallDot =
    unlines
        [ "digraph g {"
        , "\t__start0 [label=\"\" shape=none];"
        , "\ts0 [label=\"s0\"];"
        , "\ts0 -> s2 [label=\"b/x\"];"
        , "\ts0 -> s1 [label=\"a/y\"];"
        , "\ts1 -> s1 [label=\"b/x\"];"
        , "\ts1 -> s0 [label=\"a/x\"];"
        , "\ts2 -> s2 [label=\"a/y\"];"
        , "\ts2 -> s0 [label=\"b/y\"];"
        , "\t__start0 -> s0;"
        , "}"
        ]

-- 'smallDot' with the line of one transition replaced.
smallDotWith :: String -> String -> String
smallDotWith old new = unlines [if l == old then new else l | l <- lines smallDot]

-- Encode an automaton with states 0 .. n - 1 as tables, without 'Haal.Dot'.
-- 'mealyTransitions' is ordered by (state, input), which is row-major order.
encode :: MealyAutomaton Int Input Output -> (Int, String, String)
encode m = (Set.size (states m), map (chr . fst) entries, map (chr . fromEnum . snd) entries)
  where
    entries = Map.elems (mealyTransitions m)

prop_tableRoundTrip :: Mealy Input Output -> Property
prop_tableRoundTrip (Mealy m) =
    let (n, deltaTable, lambdaTable) = encode m
     in mkMealyAutomatonTable n (initial m) deltaTable lambdaTable === Right m

-- Every transition of a serialized automaton is reproduced by its table.
prop_mealyTableMatchesParsed :: Mealy Input Output -> Property
prop_mealyTableMatchesParsed (Mealy m) =
    case mealyToDot m >>= parseDot of
        Left err -> counterexample err False
        Right pm -> case mealyTable pm of
            Left err -> counterexample err False
            Right t ->
                let index names = Map.fromList (zip names [0 :: Int ..])
                    stateIdx = index (parsedStates pm)
                    inputIdx = index (parsedInputs pm)
                    outputIdx = index (parsedOutputs pm)
                    n = length (parsedStates pm)
                    k = length (parsedInputs pm)
                    keys = [(s, i) | s <- [0 .. n - 1], i <- [0 .. k - 1]]
                    decoded =
                        Map.fromList (zip keys (zip (map ord (tableDelta t)) (map ord (tableLambda t))))
                    expected =
                        Map.fromList
                            [ ((stateIdx Map.! src, inputIdx Map.! inp), (stateIdx Map.! dst, outputIdx Map.! out))
                            | (src, inp, dst, out) <- parsedTrans pm
                            ]
                 in (tableStates t, length (tableDelta t), length (tableLambda t), decoded)
                        === (n, n * k, n * k, expected)

spec :: Spec
spec = do
    describe "MealyAutomaton.mkMealyAutomatonTable" $ do
        it "rebuilds an automaton from its tables" $
            property prop_tableRoundTrip
        it "rejects tables of the wrong length" $
            (mkMealyAutomatonTable 1 0 "\0\0\0" "\0\0\0\0" :: Either String (MealyAutomaton Int Input Output))
                `shouldSatisfy` isLeft
        it "rejects transitions to states that do not exist" $
            (mkMealyAutomatonTable 1 0 "\0\0\0\1" "\0\0\0\0" :: Either String (MealyAutomaton Int Input Output))
                `shouldSatisfy` isLeft
        it "rejects outputs that do not exist" $
            (mkMealyAutomatonTable 1 0 "\0\0\0\0" "\0\0\0\4" :: Either String (MealyAutomaton Int Input Output))
                `shouldSatisfy` isLeft
        it "rejects an initial state that does not exist" $
            (mkMealyAutomatonTable 1 1 "\0\0\0\0" "\0\0\0\0" :: Either String (MealyAutomaton Int Input Output))
                `shouldSatisfy` isLeft
        it "rejects an automaton without states" $
            (mkMealyAutomatonTable 0 0 "" "" :: Either String (MealyAutomaton Int Input Output))
                `shouldSatisfy` isLeft

    describe "Dot.parseDot" $
        it "keeps states and symbols in order of first appearance" $
            fmap (\pm -> (parsedStates pm, parsedInputs pm, parsedOutputs pm)) (parseDot smallDot)
                `shouldBe` Right (["s0", "s2", "s1"], ["b", "a"], ["x", "y"])

    describe "Dot.mealyTable" $ do
        it "encodes a small automaton in row-major order" $
            (parseDot smallDot >>= mealyTable)
                `shouldBe` Right (MealyTable 3 "\1\2\0\1\2\0" "\0\1\1\1\0\0")
        it "reproduces every transition of a serialized automaton" $
            property prop_mealyTableMatchesParsed
        it "rejects an automaton with a missing transition" $
            (parseDot (smallDotWith "\ts2 -> s0 [label=\"b/y\"];" "") >>= mealyTable)
                `shouldSatisfy` either ("Incomplete" `isInfixOf`) (const False)
        it "rejects an automaton with conflicting transitions" $
            (parseDot (smallDot ++ "\ts2 -> s1 [label=\"b/y\"];\n") >>= mealyTable)
                `shouldSatisfy` either ("Nondeterministic" `isInfixOf`) (const False)
        it "accepts a duplicated identical transition" $
            (parseDot (smallDot ++ "\ts2 -> s0 [label=\"b/y\"];\n") >>= mealyTable)
                `shouldBe` (parseDot smallDot >>= mealyTable)

    describe "Dot.generateModule" $ do
        it "builds the automaton from tables" $
            (parseDot smallDot >>= generateModule "Small" "small")
                `shouldSatisfy` either (const False) ("mkMealyAutomatonTable 3 0 deltaTable lambdaTable" `isInfixOf`)
        it "rejects an incomplete automaton" $
            (parseDot (smallDotWith "\ts2 -> s0 [label=\"b/y\"];" "") >>= generateModule "Small" "small")
                `shouldSatisfy` isLeft