Skip to content

Commit 468aa9d

Browse files
authored
Tests and benchmarks for Data.Graph (#883)
* Tests and benchmarks for Data.Graph * Benchmark with more random graphs
1 parent fbafcf7 commit 468aa9d

File tree

3 files changed

+273
-0
lines changed

3 files changed

+273
-0
lines changed

containers-tests/benchmarks/Graph.hs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
module Main where
2+
3+
import Control.DeepSeq (NFData, rnf)
4+
import Control.Exception (evaluate)
5+
import Data.Array (assocs, bounds)
6+
import System.Random (mkStdGen, randomRs)
7+
import Test.Tasty.Bench (Benchmark, Benchmarkable, bench, bgroup, defaultMain, nf)
8+
import qualified Data.Graph as G
9+
10+
main :: IO ()
11+
main = do
12+
evaluate $ rnf randGs
13+
defaultMain
14+
[ bgroup "buildG" $ forGs randGs $ \g -> nf (G.buildG (bounds (getG g))) (getEdges g)
15+
, bgroup "graphFromEdges" $ forGs randGs $ nf ((\(g, _, _) -> g) . G.graphFromEdges) . getAdjList
16+
, bgroup "dfs" $ forGs randGs $ nf (flip G.dfs [1]) . getG
17+
, bgroup "dff" $ forGs randGs $ nf G.dff . getG
18+
, bgroup "topSort" $ forGs randGs $ nf G.topSort . getG
19+
, bgroup "scc" $ forGs randGs $ nf G.scc . getG
20+
, bgroup "bcc" $ forGs [randG1, randG2] $ nf G.bcc . getG
21+
, bgroup "stronglyConnCompR" $ forGs randGs $ nf G.stronglyConnCompR . getAdjList
22+
]
23+
where
24+
randG1 = buildRandG 100 1000
25+
randG2 = buildRandG 100 10000
26+
randG3 = buildRandG 10000 100000
27+
randGs = [randG1, randG2, randG3]
28+
29+
-- Note: In practice it does not make sense to run topSort or bcc on a random
30+
-- graph. For topSort the graph should be acyclic and for bcc the graph should
31+
-- be undirected. But these functions don't check or depend on these properties,
32+
-- so we can keep things simple and run them on random graphs in benchmarks.
33+
34+
forGs :: [Graph] -> (Graph -> Benchmarkable) -> [Benchmark]
35+
forGs gs f = [bench (getLabel g) (f g) | g <- gs]
36+
37+
data Graph = Graph
38+
{ getLabel :: String
39+
, getG :: G.Graph
40+
, getEdges :: [(G.Vertex, G.Vertex)]
41+
, getAdjList :: [(Int, G.Vertex, [G.Vertex])]
42+
}
43+
44+
instance NFData Graph where
45+
rnf (Graph label g edges adj) = rnf label `seq` rnf g `seq` rnf edges `seq` rnf adj
46+
47+
-- A graph with vertices [1..n] and m random edges.
48+
buildRandG :: Int -> Int -> Graph
49+
buildRandG n m = Graph label g (G.edges g) [(u, u, vs') | (u, vs') <- assocs g]
50+
where
51+
label = "n=" ++ show n ++ ",m=" ++ show m
52+
xs = randomRs (1, n) (mkStdGen 1)
53+
(us, xs') = splitAt m xs
54+
vs = take m xs'
55+
g = G.buildG (1, n) (zip us vs)

containers-tests/containers-tests.cabal

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,16 @@ benchmark set-benchmarks
171171
main-is: Set.hs
172172
ghc-options: -O2
173173

174+
benchmark graph-benchmarks
175+
import: benchmark-deps
176+
default-language: Haskell2010
177+
type: exitcode-stdio-1.0
178+
hs-source-dirs: benchmarks
179+
main-is: Graph.hs
180+
ghc-options: -O2
181+
build-depends:
182+
random >=0 && <1.2
183+
174184
benchmark set-operations-intmap
175185
import: benchmark-deps
176186
default-language: Haskell2010
@@ -351,6 +361,14 @@ test-suite tree-properties
351361
BangPatterns
352362
CPP
353363

364+
test-suite graph-properties
365+
import: test-deps
366+
default-language: Haskell2010
367+
hs-source-dirs: tests
368+
main-is: graph-properties.hs
369+
type: exitcode-stdio-1.0
370+
ghc-options: -O2
371+
354372
test-suite map-strictness-properties
355373
import: test-deps
356374
default-language: Haskell2010
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import Data.Array (bounds, listArray)
2+
import Data.Maybe (fromJust)
3+
import Test.Tasty
4+
import Test.Tasty.HUnit
5+
import Test.Tasty.QuickCheck
6+
import qualified Data.Foldable as F
7+
import qualified Data.Graph as G
8+
import qualified Data.List as L
9+
import qualified Data.Set as S
10+
11+
default (Int)
12+
13+
main :: IO ()
14+
main = defaultMain $ testGroup "graph-properties"
15+
[ testCase "buildG" test_buildG
16+
, testCase "graphFromEdges" test_graphFromEdges
17+
, testCase "dfs" test_dfs
18+
, testCase "dff" test_dff
19+
20+
, testProperty "prop_dfs" prop_dfs
21+
, testProperty "prop_dff" prop_dff
22+
, testProperty "prop_topSort" prop_topSort
23+
, testProperty "prop_scc" prop_scc
24+
, testProperty "prop_bcc" prop_bcc
25+
, testProperty "prop_stronglyConnCompR" prop_stronglyConnCompR
26+
]
27+
28+
----------------------------------------------------------------
29+
-- Arbitrary graphs
30+
----------------------------------------------------------------
31+
32+
newtype Graph = Graph G.Graph deriving Show
33+
34+
instance Arbitrary Graph where
35+
arbitrary = sized $ \sz0 -> do
36+
sz <- choose (0, sz0)
37+
l <- arbitrary
38+
let u = l + sz - 1
39+
edges <- if sz == 0
40+
then pure []
41+
else listOf $ (,) <$> choose (l,u) <*> choose (l,u)
42+
pure $ Graph $ G.buildG (l,u) edges
43+
44+
-- Directed acyclic graph
45+
newtype DAG = DAG G.Graph deriving Show
46+
47+
instance Arbitrary DAG where
48+
arbitrary = sized $ \sz0 -> do
49+
sz <- choose (0, sz0)
50+
l <- arbitrary
51+
let u = l + sz - 1
52+
vs <- shuffle [l..u]
53+
-- edges are directed in the order in which their vertices appear in vs
54+
edges <- if sz <= 1
55+
then pure []
56+
else listOf $ ((,) <$> choose (l,u) <*> choose (l,u)) `suchThat`
57+
\(from, to) -> fromJust (L.elemIndex from vs) < fromJust (L.elemIndex to vs)
58+
pure $ DAG $ G.buildG (l,u) edges
59+
60+
-- A graph where for every edge (u,v), the reverse edge (v,u) exists
61+
newtype UndirectedG = UndirectedG G.Graph deriving Show
62+
63+
instance Arbitrary UndirectedG where
64+
arbitrary = do
65+
Graph g <- arbitrary
66+
let edges = G.edges g
67+
pure $ UndirectedG $ G.buildG (bounds g) (edges ++ [(v,u) | (u,v) <- edges])
68+
69+
newtype AdjList node key = AdjList [(node, key, [key])] deriving Show
70+
71+
instance (Arbitrary node, Arbitrary key, Eq key) => Arbitrary (AdjList node key) where
72+
arbitrary = do
73+
keys <- L.nub <$> arbitrary
74+
keyss <- vectorOf (length keys) arbitrary
75+
nodes <- vectorOf (length keys) arbitrary
76+
pure $ AdjList $ zip3 nodes keys keyss
77+
78+
----------------------------------------------------------------
79+
-- Unit tests
80+
----------------------------------------------------------------
81+
82+
test_buildG :: Assertion
83+
test_buildG = do
84+
G.buildG (1,0) [] @?= listArray (1,0) []
85+
G.buildG (1,1) [(1,1), (1,1), (1,1)] @?= listArray (1,1) [[1, 1, 1]]
86+
G.buildG (1,3) [(1,2), (1,3), (2,3)] @?= listArray (1,3) [[3, 2], [3], []]
87+
G.buildG (1,3) [(1,2), (1,3), (2,1), (2,3), (3,1), (3,2)] @?= listArray (1, 3) [[3, 2], [3, 1], [2, 1]]
88+
89+
test_graphFromEdges :: Assertion
90+
test_graphFromEdges = do
91+
let (graph1, _, _) = G.graphFromEdges ([] :: [(Int, Int, [Int])])
92+
graph1 @?= listArray (0,-1) []
93+
94+
let (graph2, nodeFromVertex2, vertexFromKey2) = G.graphFromEdges [('a', 10, [10])]
95+
graph2 @?= listArray (0,0) [[0]]
96+
nodeFromVertex2 0 @?= ('a', 10, [10])
97+
vertexFromKey2 10 @?= Just 0
98+
99+
let (graph3, nodeFromVertex3, vertexFromKey3) = G.graphFromEdges [('b', 20, [30, 40]), ('a', 10, [20, 30, 40]), ('d', 40, []), ('c', 30, [40])]
100+
graph3 @?= listArray (0,3) [[1, 2, 3], [2, 3], [3], []]
101+
map nodeFromVertex3 [0..3] @?= [('a', 10, [20, 30, 40]), ('b', 20, [30, 40]), ('c', 30, [40]), ('d', 40, [])]
102+
map vertexFromKey3 [10, 20, 30, 40] @?= map Just [0..3]
103+
104+
test_dfs :: Assertion
105+
test_dfs = do
106+
G.dfs (G.buildG (1,0) []) [] @?= []
107+
G.dfs (G.buildG (1,1) [(1,1), (1,1), (1,1)]) [1] @?= [G.Node 1 []]
108+
G.dfs (G.buildG (1,3) [(1,2), (1,3), (2,3)]) [1] @?= [G.Node 1 [G.Node 3 [], G.Node 2 []]]
109+
G.dfs (G.buildG (1,3) [(1,2), (1,3), (2,3)]) [2] @?= [G.Node 2 [G.Node 3 []]]
110+
G.dfs (G.buildG (1,3) [(1,2), (1,3), (2,3)]) [3] @?= [G.Node 3 []]
111+
G.dfs (G.buildG (1,3) [(1,2), (1,3), (2,3)]) [3,2,1] @?= [G.Node 3 [], G.Node 2 [], G.Node 1 []]
112+
113+
test_dff :: Assertion
114+
test_dff = do
115+
G.dff (G.buildG (1,0) []) @?= []
116+
G.dff (G.buildG (1,1) [(1,1), (1,1), (1,1)]) @?= [G.Node 1 []]
117+
G.dff (G.buildG (1,3) [(1,2), (1,3), (2,3)]) @?= [G.Node 1 [G.Node 3 [], G.Node 2 []]]
118+
G.dff (G.buildG (1,3) [(1,2), (1,3), (2,1), (2,3), (3,1), (3,2)]) @?= [G.Node 1 [G.Node 3 [G.Node 2 []]]]
119+
120+
----------------------------------------------------------------
121+
-- QuickCheck
122+
----------------------------------------------------------------
123+
124+
-- Note: This tests some simple properties but not complete correctness
125+
prop_dfs :: Graph -> Property
126+
prop_dfs (Graph g) =
127+
let vsgen = if null (G.vertices g) then pure [] else listOf $ choose (bounds g)
128+
in forAll vsgen $ \vs ->
129+
let ts = G.dfs g vs
130+
in S.fromList (concatMap F.toList ts) `S.isSubsetOf` S.fromList (G.vertices g) .&&.
131+
S.fromList (concatMap treeEdges ts) `S.isSubsetOf` S.fromList (G.edges g)
132+
133+
-- Note: This tests some simple properties but not complete correctness
134+
prop_dff :: Graph -> Property
135+
prop_dff (Graph g) =
136+
let ts = G.dff g
137+
in L.sort (concatMap F.toList ts) === G.vertices g .&&.
138+
S.fromList (concatMap treeEdges ts) `S.isSubsetOf` S.fromList (G.edges g)
139+
140+
prop_topSort :: DAG -> Property
141+
prop_topSort (DAG g) =
142+
let vs = G.topSort g
143+
in L.sort vs === G.vertices g .&&.
144+
and [not (G.path g v u) | u:vs' <- L.tails vs, v <- vs']
145+
146+
prop_scc :: Graph -> Property
147+
prop_scc (Graph g) =
148+
let ts = G.scc g
149+
in L.sort (concatMap F.toList ts) === G.vertices g .&&.
150+
S.fromList (concatMap treeEdges ts) `S.isSubsetOf` S.fromList (G.edges g) .&&.
151+
-- vertices in a component are mutually reachable
152+
and [G.path g u v | t <- ts, u <- F.toList t, v <- F.toList t] .&&.
153+
-- vertices in later components are not reachable from earlier components, due to reverse
154+
-- topological order
155+
and [not (G.path g u v) | t:ts' <- L.tails ts, u <- F.toList t, v <- concatMap F.toList ts']
156+
157+
prop_bcc :: UndirectedG -> Property
158+
prop_bcc (UndirectedG g) =
159+
let ts = G.bcc g
160+
comps = concatMap F.toList ts :: [[G.Vertex]]
161+
in S.fromList (concat comps) `S.isSubsetOf` S.fromList (G.vertices g) .&&.
162+
all testBCC comps .&&.
163+
all (uncurry testBCCs) (concatMap treeEdges ts)
164+
where
165+
-- a biconnected component remains connected even if any single vertex is removed
166+
testBCC c = and [subsetComponents (L.delete x c) == 1 | x <- c]
167+
-- adjacent biconnected components are connected, but become disconnected if their common
168+
-- vertex is removed
169+
testBCCs c1 c2 = case c1 `L.intersect` c2 of
170+
[x] -> subsetComponents (c1 ++ c2) == 1 &&
171+
subsetComponents ((c1 ++ c2) L.\\ [x, x]) == 2
172+
_ -> False
173+
-- the number of components in the given subset of vertices
174+
subsetComponents xs =
175+
let g' = G.buildG (bounds g) [(u,v) | (u,v) <- G.edges g, u `elem` xs && v `elem` xs]
176+
in length (G.dfs g' xs)
177+
178+
prop_stronglyConnCompR :: AdjList Int Int -> Property
179+
prop_stronglyConnCompR (AdjList adj) =
180+
let comps = G.stronglyConnCompR adj
181+
in L.sort (G.flattenSCCs comps) === L.sort adj .&&.
182+
all testSCC comps .&&.
183+
-- vertices in later components are not reachable from earlier components, due to reverse
184+
-- topological order
185+
and [ not (G.path g (getv k) (getv k'))
186+
| c:cs <- L.tails comps
187+
, (_,k,_) <- G.flattenSCC c
188+
, (_,k',_) <- G.flattenSCCs cs
189+
]
190+
where
191+
(g, _, vertexFromKey) = G.graphFromEdges adj
192+
getv = fromJust . vertexFromKey
193+
-- vertices in a cyclic component are mutually reachable
194+
testSCC (G.AcyclicSCC (_, k, ks)) = k `notElem` ks
195+
testSCC (G.CyclicSCC [(_, k, ks)]) = k `elem` ks
196+
testSCC (G.CyclicSCC xs) = and [G.path g (getv k) (getv k') | (_,k,_) <- xs , (_,k',_) <- xs]
197+
198+
treeEdges :: G.Tree a -> [(a, a)]
199+
treeEdges t = go t []
200+
where go (G.Node x ts) acc = [(x,y) | G.Node y _ <- ts] ++ foldr go acc ts

0 commit comments

Comments
 (0)