packages feed

haggle-0.3.1: tests/GraphTests.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-missing-signatures #-}
{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}

-- | This module tests Haggle by comparing its results to those of FGL.
-- This assumes that FGL is reasonably correct.
--
-- The arbitrary instance for GraphPair generates a list of edges and
-- then constructs equivalent FGL and Haggle graphs.  The quickcheck
-- properties for each operation try to ensure that the two implementations
-- return the same results.

module Main ( main ) where

import Test.Framework ( defaultMain, Test )
import Test.Framework.Providers.QuickCheck2 ( testProperty )
import Test.Framework.Providers.HUnit ( hUnitTestToTests )
import Test.HUnit
import Test.QuickCheck

import Control.Arrow ( first, second )
import qualified Data.Bifunctor as Bi
import Control.Monad ( replicateM )
import Data.Function ( on )
import qualified Data.Foldable as F
import qualified Data.List as L
import Data.Maybe ( fromJust, isNothing )
import qualified Data.Set as S
#if MIN_VERSION_base(4, 11, 0)
#else
import           Data.Monoid ( (<>) )
#endif

import qualified Data.Graph.Inductive as FGL
import qualified Data.Graph.Haggle as HGL
import qualified Data.Graph.Haggle.Algorithms.DFS as HGL
import qualified Data.Graph.Haggle.Algorithms.Dominators as HGL

-- import Debug.Trace
-- debug = flip trace

type BaseGraph = FGL.Gr Int ()
type TestGraph = HGL.VertexLabeledGraph HGL.SimpleBiDigraph Int

data GraphPair = GP [(Int, Int)] BaseGraph TestGraph

instance Arbitrary GraphPair where
  arbitrary = sized mkGraphPair

instance Show GraphPair where
  show (GP es _ _) = show es

newtype NodeId = NID Int
  deriving (Show)
instance Arbitrary NodeId where
  arbitrary = sized mkNodeId
    where
      mkNodeId n = do
        i <- choose (0, n)
        return (NID i)

-- | Generates a pair of a Haggle graph and the corresponding FGL graph to serve
-- as an oracle.
mkGraphPair :: Int -> Gen GraphPair
mkGraphPair sz = do
  nEdges <- choose (2, 2 * sz)
  srcs <- replicateM nEdges (choose (0, sz))
  dsts <- replicateM nEdges (choose (0, sz))
  let edges = unique $ zip srcs dsts
      nids = unique (srcs ++ dsts)
      ns = zip nids nids
      bg = FGL.mkGraph ns (map (\(s, d) -> (s, d, ())) edges)
      (tg, _) = HGL.fromEdgeList HGL.newMSimpleBiDigraph edges
  return $! GP edges bg tg



main :: IO ()
main = defaultMain tests

tests :: [Test.Framework.Test]
tests = [ testProperty "prop_sameVertexCount" prop_sameVertexCount
        , testProperty "prop_sameEdgeCount" prop_sameEdgeCount
        , testProperty "prop_sameSuccessorsAtLabel" prop_sameSuccessorsAtLabel
        , testProperty "prop_samePredecessorsAtLabel" prop_samePredecessorsAtLabel
        , testProperty "prop_dfsSame" prop_dfsSame
        , testProperty "prop_sameComponents" prop_sameComponents
        , testProperty "prop_sameNoComponents" prop_sameNoComponents
        , testProperty "prop_immDominatorsSame" prop_immDominatorsSame

        -- prop_dominatorsSame requires fgl 5.8.1.1, which fixes errors in fgl's
        -- dom functionality that is used as the oracle for the tests here.
        , testProperty "prop_dominatorsSame" prop_dominatorsSame

        , testProperty "patricia match: remaining vertices" prop_match_patricia_remvertices
        , testProperty "patricia match: vertex label removed" prop_match_patricia_vlblremoved
        , testProperty "patricia match: disconnected to" prop_match_patricia_no_in_edges
        , testProperty "patricia match: disconnected from" prop_match_patricia_no_out_edges
        , testProperty "patricia match: edges removed" prop_match_patricia_remedges
        ] <>  testPatricia
        <> testExplicit

prop_sameVertexCount :: GraphPair -> Bool
prop_sameVertexCount (GP _ bg tg) =
  length (FGL.nodes bg) == length (HGL.vertices tg)

prop_sameEdgeCount :: GraphPair -> Bool
prop_sameEdgeCount (GP _ bg tg) =
  length (FGL.edges bg) == length (HGL.edges tg)

prop_sameSuccessorsAtLabel :: (NodeId, GraphPair) -> Bool
prop_sameSuccessorsAtLabel (NID nid, GP _ bg tg)
  | not (FGL.gelem nid bg) && isNothing (vertexFromLabel tg nid) = True
  | otherwise = bss == tss
  where
    bss = S.fromList $ fmap Just $ FGL.suc bg nid
    ts = maybe [] (map (HGL.vertexLabel tg) . HGL.successors tg) (vertexFromLabel tg nid)
    tss = S.fromList ts

prop_samePredecessorsAtLabel :: (NodeId, GraphPair) -> Bool
prop_samePredecessorsAtLabel (NID nid, GP _ bg tg)
  | not (FGL.gelem nid bg) && isNothing (vertexFromLabel tg nid) = True
  | otherwise = bss == tss
  where
    bss = S.fromList $ fmap Just $ FGL.pre bg nid
    ts = maybe [] (map (HGL.vertexLabel tg) . HGL.predecessors tg) (vertexFromLabel tg nid)
    tss = S.fromList ts

-- Note that this is only checking the *set* of vertices reached.  Unfortunately,
-- verifying the *order* is difficult because there are many valid DFS orders
-- (depending on the order edges are stored).  A test using the DFS number
-- (derived from the depth in the depth-first tree) would be a good complement
-- to this.
prop_dfsSame :: (NodeId, GraphPair) -> Bool
prop_dfsSame (NID root, GP _ bg tg) =
  S.fromList bres == S.fromList tres
  where
    bres = map Just $ FGL.dfs [root] bg
    v = vertexFromLabel tg root
    tres = maybe [] (map (HGL.vertexLabel tg) . HGL.dfs tg . (:[])) v

prop_immDominatorsSame :: (NodeId, GraphPair) -> Bool
prop_immDominatorsSame (NID root, GP _ bg tg)
  | not (FGL.gelem root bg) && isNothing (vertexFromLabel tg root) = True
  | otherwise = S.fromList bdoms == S.fromList tdoms
  where
    bdoms = FGL.iDom bg root
    toLabs (v1, v2) =
      let Just v1l = HGL.vertexLabel tg v1
          Just v2l = HGL.vertexLabel tg v2
      in (v1l, v2l)
    tdoms = maybe [] (map toLabs . HGL.immediateDominators tg) (vertexFromLabel tg root)

prop_dominatorsSame :: (NodeId, GraphPair) -> Bool
prop_dominatorsSame (NID root, GP _ bg tg)
  | not (FGL.gelem root bg) && isNothing (vertexFromLabel tg root) = True
  | otherwise = S.fromList (map (first Just) bdoms) == S.fromList (map (first (HGL.vertexLabel tg)) tdoms)
  where
    bdoms = map (second (S.fromList . map Just)) $ FGL.dom bg root
    Just rv = vertexFromLabel tg root
    tdoms = map (second (S.fromList . map (HGL.vertexLabel tg))) $ HGL.dominators tg rv

prop_sameComponents :: GraphPair -> Bool
prop_sameComponents (GP _ bg tg) = bcs == tcs
  where
    bcs = S.map (S.fromList . map Just) $ S.fromList $ FGL.components bg
    tcs = S.map (S.fromList . map (HGL.vertexLabel tg)) $ S.fromList $ HGL.components tg

prop_sameNoComponents :: GraphPair -> Bool
prop_sameNoComponents (GP _ bg tg) =
  FGL.noComponents bg == HGL.noComponents tg

-- Helpers

vertexFromLabel :: TestGraph -> Int -> Maybe HGL.Vertex
vertexFromLabel g lbl = F.find labelMatch (HGL.vertices g)
  where
    labelMatch v = Just lbl == (HGL.vertexLabel g v)

unique :: (Ord a) => [a] -> [a]
unique = S.toList . S.fromList

----------------------------------------------------------------------

-- Explicit tests for various functionality

testExplicit :: [Test.Framework.Test]
testExplicit =
  let gr0 = foldl (\g -> snd . HGL.insertLabeledVertex g)
            (HGL.emptyGraph :: HGL.PatriciaTree Int Char)
            [1,2,4]
      vs = fst <$> HGL.labeledVertices gr0

      plusEdge g f t = snd $ fromJust $ HGL.insertLabeledEdge g f t 'a'

      gr1 = plusEdge gr0 (vs !! 0) (vs !! 1)
      -- gr1 has three nodes, two are connected, one is not connected (i.e. two
      -- independent subgraphs)

      gr2 = plusEdge (plusEdge gr0 (vs !! 1) (vs !! 0)) (vs !! 1) (vs !! 1)


  in hUnitTestToTests $ test
     [ "haggle (patricia) [1-2,4] reachable from 1" ~:
       do HGL.reachable (vs !! 0) gr1 @?= [ (vs !! 0)
                                          , (vs !! 1)
                                          ]
     , "haggle (patricia) [1-2,4] reachable from 2" ~:
       do HGL.reachable (vs !! 1) gr1 @?= [ (vs !! 1)
                                          ]
     , "haggle (patricia) [2-1,2-2] reachable from 1" ~:
       do HGL.reachable (vs !! 0) gr2 @?= [ (vs !! 0)
                                          ]
     , "no cycle in [1-2,4] from 1" ~:
       do HGL.hasCycle (vs !! 0) gr1 @?= False

     , "cycle in [1-2-1,4] from 1" ~:
       do let tg = plusEdge gr1 (vs !! 1) (vs !! 0)
          HGL.hasCycle (vs !! 0) tg @?= True

     , "cycle in [1*-2,4] from 1" ~:
       do let tg = plusEdge gr1 (vs !! 0) (vs !! 0)
          HGL.hasCycle (vs !! 0) tg @?= True

     , "haggle dominator [1-2,4] from 1" ~:
       do HGL.dominators gr1 (vs !! 0) @?= [ (vs !! 0, [ (vs !! 0) ])
                                           , (vs !! 1, [ (vs !! 1), (vs !! 0) ])
                                           ]

     , "haggle dominator [2-1,2-2,4] from 1" ~:
       do HGL.dominators gr2 (vs !! 0) @?= [ (vs !! 0, [ (vs !! 0) ])
                                           ]

     , "haggle add self-edge" ~:
       do let Just (e,g) = HGL.insertLabeledEdge gr0 (vs!!0) (vs!!0) 's'
          HGL.edges g @?= [e]

     , "haggle delete self-edge" ~:
       do let Just (_,g) = HGL.insertLabeledEdge gr0 (vs!!0) (vs!!0) 's'
          HGL.edges (HGL.deleteEdgesBetween g (vs!!0) (vs!!0)) @?= []


     -- n.b. fgl's dominator is broken (as haggle's original version also was) in
     -- that its return includes (4, [1,2,4]), which is invalid: 4 is in an
     -- independent subgraph and cannot be dominated by 1.
     -- , "fgl dominator for [1-2,4] from 1" ~:
     --   do let fgr0 = FGL.mkGraph [(1,1), (2,2), (4,4)] [(0, 1, 'f')] :: FGL.Gr Int Char
     --      FGL.dom fgr0 1 @?= [ (1, [ 1 ])
     --                         , (2, [ 1, 2 ])
     --                         ]

     -- n.b. fgl's dominator is also broken in regards to reachability.  For a
     -- dom return for [2-1, 2-2] from 1 also returns (2, [1,2]) which is
     -- invalid, because 2 is not reachable from 1 and so 1 cannot be a dominator
     -- for 2
     -- , "fgl dominator for [2-1,2-2] from 1" ~:
     --   do let fgr2 = FGL.mkGraph [(1,1), (2,2)] [(2,1,'f'), (2,2,'s')] :: FGL.Gr Int Char
     --      FGL.dom fgr2 1 @?= [ (1, [ 1 ]) ]
     ]

testPatricia :: [Test.Framework.Test]
testPatricia =
  let gr0 = foldl (\g -> snd . HGL.insertLabeledVertex g)
            (HGL.emptyGraph :: HGL.PatriciaTree Int Char)
            [1,2,4,3,5,0]
      vs = fst <$> (L.sortBy (compare `on` snd) $ HGL.labeledVertices gr0)
      gr1 = foldl (\g (f,t,l) ->
                     snd $ fromJust $ HGL.insertLabeledEdge g f t l)
            gr0
            [ (vs !! 2, vs !! 4, 'a')
            , (vs !! 1, vs !! 4, 'b')
            , (vs !! 2, vs !! 0, 'c')
            ]
  in hUnitTestToTests $ test
     [ "create graph" ~:
       do sum (snd <$> HGL.labeledVertices gr1) @?= 15
          L.sort (snd <$> HGL.labeledEdges gr1) @?= "abc"

     , "bifunctor first (nodes)" ~:
       do let gr2 = Bi.first (+3) gr1
          sum (snd <$> HGL.labeledVertices gr2) @?= 33
          L.sort (snd <$> HGL.labeledEdges gr2) @?= "abc"

     , "bifunctor second (edges)" ~:
       do let gr2 = Bi.second (succ . succ . succ) gr1
          sum (snd <$> HGL.labeledVertices gr2) @?= 15
          L.sort (snd <$> HGL.labeledEdges gr2) @?= "def"

     , "bifunctor bimap" ~:
       do let gr2 = Bi.bimap (+2) (succ . succ) gr1
          sum (snd <$> HGL.labeledVertices gr2) @?= 27
          L.sort (snd <$> HGL.labeledEdges gr2) @?= "cde"

     , "replaceLabeledVertex" ~:
       do let gr2 = HGL.replaceLabeledVertex gr1 (vs !! 4) 11
          -- Vertex label changed?
          sum (snd <$> HGL.labeledVertices gr2) @?= (15 + (11 - 4))
          -- Edges are still in place?
          L.sort (snd <$> HGL.labeledEdges gr2) @?= "abc"
     ]


----------------------------------------------------------------------


newtype NodeLabel = NL Int deriving (Eq, Show)
newtype EdgeLabel = EL Int deriving (Eq, Show)

-- type InductiveGraphBuilder g = (g NodeLabel EdgeLabel -> g NodeLabel EdgeLabel)
data InductiveGraphBuilder g =
  IGB { build :: g NodeLabel EdgeLabel -> g NodeLabel EdgeLabel }

instance ( HGL.InductiveGraph (g NodeLabel EdgeLabel)
         , HGL.HasVertexLabel (g NodeLabel EdgeLabel)
         , HGL.HasEdgeLabel (g NodeLabel EdgeLabel)
         , HGL.VertexLabel (g NodeLabel EdgeLabel) ~ NodeLabel
         , HGL.EdgeLabel (g NodeLabel EdgeLabel) ~ EdgeLabel
         ) => Arbitrary (InductiveGraphBuilder g) where
  arbitrary = oneof [ solitaryNode
                    , edgeToNewNode
                    , edgeBetweenExistingNodes
                    , edgeToSelf
                    ]
    where solitaryNode = return $ IGB $ \g ->
            let vLabel = NL $ length $ HGL.vertices g
            in snd $ HGL.insertLabeledVertex g vLabel
          edgeToNewNode = do
            srcNum <- choose (0, 1024)
            return $ IGB $ \g ->
              let vs = HGL.vertices g
                  srcV = cycle vs !! srcNum
                  vLabel = NL $ length $ vs
                  eLabel = EL $ length $ HGL.edges g
                  (nv, ng) = HGL.insertLabeledVertex g vLabel
              in if null vs
                 then ng
                 else maybe g snd $ HGL.insertLabeledEdge ng srcV nv eLabel
          edgeBetweenExistingNodes = do
            srcNum <- choose (0, 1024)
            dstNum <- choose (0, 1024)
            -- n.b. the inductive graphs don't like duplicated edges, but they
            -- will just return Nothing on inserting the edge, which returns the
            -- existing graph, so this duplication attempt is quietly ignored.
            return $ IGB $ \g ->
              let vs = HGL.vertices g
                  srcV = cycle (HGL.vertices g) !! srcNum
                  dstV = cycle (HGL.vertices g) !! dstNum
                  eLabel = EL $ length $ HGL.edges g
              in if null vs
                 then snd $ HGL.insertLabeledVertex g $ NL 0
                 else maybe g snd $ HGL.insertLabeledEdge g srcV dstV eLabel
          edgeToSelf = do
            srcNum <- choose (0, 1024)
            -- see note above re: duplicate edges
            return $ IGB $ \g ->
              let vs = HGL.vertices g
                  srcV = cycle (HGL.vertices g) !! srcNum
                  eLabel = EL $ length $ HGL.edges g
              in if null vs
                 then snd $ HGL.insertLabeledVertex g $ NL 0
                 else maybe g snd $ HGL.insertLabeledEdge g srcV srcV eLabel


type InductiveProperty g = InductiveCase g -> Bool

data InductiveCase g = IGC g HGL.Vertex deriving Show

instance (Arbitrary g, HGL.Graph g) => Arbitrary (InductiveCase g) where
  arbitrary = do g <- arbitrary
                 v <- elements $ HGL.vertices g
                 return $ IGC g v

onMatchResult :: HGL.InductiveGraph g
              => HGL.Graph g
              => (g -> HGL.Vertex -> (HGL.Context g, g) -> Bool)
              -> InductiveProperty g
onMatchResult prop (IGC g v) =
  case HGL.match g v of
    Nothing -> False
    Just mr -> prop g v mr

prop_match_inductive_remvertices :: HGL.InductiveGraph g => InductiveProperty g
prop_match_inductive_remvertices = onMatchResult $ \g -> \_ -> \(_ctxt, g') ->
  length (HGL.vertices g) == length (HGL.vertices g') + 1

prop_match_inductive_vlblremoved :: HGL.InductiveGraph g
                                 => Eq (HGL.VertexLabel g)
                                 => InductiveProperty g
prop_match_inductive_vlblremoved = onMatchResult $ \_ -> \v -> \(ctxt, g') ->
  let HGL.Context _ vl _ = ctxt
  in not $ (v,vl) `elem` HGL.labeledVertices g'

prop_match_inductive_no_in_edges :: HGL.InductiveGraph g
                                 => InductiveProperty g
prop_match_inductive_no_in_edges = onMatchResult $ \_ -> \v -> \(ctxt, g') ->
  let HGL.Context intos _ _ = ctxt
      edgeInTo (_,sv) = v /= sv && v `elem` HGL.successors g' sv
  in not $ any edgeInTo intos

prop_match_inductive_no_out_edges :: HGL.InductiveGraph g
                                  => HGL.Bidirectional g
        => Show g
                                  => InductiveProperty g
prop_match_inductive_no_out_edges = onMatchResult $ \_ -> \v -> \(ctxt, g') ->
  let HGL.Context _ _ outs = ctxt
      edgeOutTo (_,dv) = v /= dv && v `elem` HGL.predecessors g' dv
  in not $ any edgeOutTo outs

prop_match_inductive_remedges :: HGL.InductiveGraph g
                              => HGL.HasEdgeLabel g
                              => Eq (HGL.EdgeLabel g)
                              => InductiveProperty g
prop_match_inductive_remedges = onMatchResult $ \_ -> \_ -> \(ctxt, g') ->
  let HGL.Context intos _ outs = ctxt
      remainingEdgeLabels = snd <$> HGL.labeledEdges g'
      hasEdge (el,_) = el `elem` remainingEdgeLabels
  in not $ any hasEdge $ intos <> outs

--------------------

type PatriciaProperty = InductiveProperty (HGL.PatriciaTree NodeLabel EdgeLabel)

instance Arbitrary (HGL.PatriciaTree NodeLabel EdgeLabel) where
  arbitrary = do mkGraph <- listOf1 arbitrary
                 return $ foldr build HGL.emptyGraph mkGraph

instance Show (HGL.PatriciaTree NodeLabel EdgeLabel) where
  show g = "PatriciaTree/" <> show (length $ HGL.vertices g)
           <> "/" <> show (length $ HGL.edges g)

prop_match_patricia_remvertices :: PatriciaProperty
prop_match_patricia_remvertices = prop_match_inductive_remvertices

prop_match_patricia_vlblremoved :: PatriciaProperty
prop_match_patricia_vlblremoved = prop_match_inductive_vlblremoved

prop_match_patricia_no_in_edges :: PatriciaProperty
prop_match_patricia_no_in_edges = prop_match_inductive_no_in_edges

prop_match_patricia_no_out_edges :: PatriciaProperty
prop_match_patricia_no_out_edges = prop_match_inductive_no_out_edges

prop_match_patricia_remedges :: PatriciaProperty
prop_match_patricia_remedges = prop_match_inductive_remedges