Skip to content

Commit bac841d

Browse files
initial stab at property tests for Constrained.Graph
1 parent b24b667 commit bac841d

File tree

4 files changed

+117
-4
lines changed

4 files changed

+117
-4
lines changed

constrained-generators.cabal

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,9 @@ test-suite constrained-tests
125125
type: exitcode-stdio-1.0
126126
main-is: Tests.hs
127127
hs-source-dirs: test
128-
other-modules: Constrained.Tests
128+
other-modules:
129+
Constrained.Tests
130+
Constrained.GraphSpec
129131
default-language: Haskell2010
130132
ghc-options:
131133
-Wall

src/Constrained/Graph.hs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
-- | This module provides a dependency graph implementation.
66
module Constrained.Graph (
77
Graph,
8+
edges,
9+
opEdges,
810
opGraph,
11+
mkGraph,
912
nodes,
1013
deleteNode,
1114
subtractGraph,
@@ -29,13 +32,14 @@ import Data.Maybe
2932
import Data.Set (Set)
3033
import Data.Set qualified as Set
3134
import Prettyprinter
35+
import Test.QuickCheck
3236

3337
-- | A graph with unlabeled edges for keeping track of dependencies
3438
data Graph node = Graph
3539
{ edges :: !(Map node (Set node))
3640
, opEdges :: !(Map node (Set node))
3741
}
38-
deriving (Show)
42+
deriving (Show, Eq)
3943

4044
instance Ord node => Semigroup (Graph node) where
4145
Graph e o <> Graph e' o' =
@@ -55,6 +59,28 @@ instance Pretty n => Pretty (Graph n) where
5559
| (n, ns) <- Map.toList (edges gr)
5660
]
5761

62+
-- | Construct a graph
63+
mkGraph :: Ord node => Map node (Set node) -> Graph node
64+
mkGraph e0 = Graph e $ Map.unionsWith (<>)
65+
[ Map.fromList $ (p, mempty) : [ (c, Set.singleton p)
66+
| c <- Set.toList cs
67+
]
68+
| (p, cs) <- Map.toList e
69+
]
70+
where e = Map.unionWith (<>) e0 (Map.fromList [ (c, mempty) | (_, cs) <- Map.toList e0
71+
, c <- Set.toList cs
72+
])
73+
74+
instance (Arbitrary node, Ord node) => Arbitrary (Graph node) where
75+
-- TODO: very high % of these graphs don't topsort nicely
76+
arbitrary = mkGraph <$> arbitrary
77+
shrink g =
78+
[ mkGraph e'
79+
| e <- shrink (edges g)
80+
-- If we don't do this it's very easy to introduce a shrink-loop
81+
, let e' = fmap (\ xs -> Set.filter (`Map.member` e) xs) e
82+
]
83+
5884
-- | Get all the nodes of a graph
5985
nodes :: Graph node -> Set node
6086
nodes (Graph e _) = Map.keysSet e

test/Constrained/GraphSpec.hs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
{-# LANGUAGE DerivingStrategies #-}
2+
{-# LANGUAGE ScopedTypeVariables #-}
3+
{-# LANGUAGE NumericUnderscores #-}
4+
{-# LANGUAGE ImportQualifiedPost #-}
5+
{-# LANGUAGE DerivingVia #-}
6+
module Constrained.GraphSpec where
7+
8+
import Data.Either
9+
import Constrained.Graph
10+
import Data.Set (Set)
11+
import Data.Set qualified as Set
12+
import Test.Hspec
13+
import Test.Hspec.QuickCheck
14+
import Test.QuickCheck
15+
16+
newtype Node = Node Int
17+
deriving (Ord, Eq)
18+
deriving Show via Int
19+
20+
instance Arbitrary Node where
21+
arbitrary = Node <$> choose (0, 20)
22+
shrink (Node n) = Node <$> shrink n
23+
24+
prop_no_dependencies_topsort :: Set Node -> Property
25+
prop_no_dependencies_topsort = property . isRight . topsort . noDependencies
26+
27+
prop_subtract_topsort :: Graph Node -> Graph Node -> Property
28+
prop_subtract_topsort g g' =
29+
isRight (topsort g) ==>
30+
isRight (topsort $ subtractGraph g g')
31+
32+
prop_delete_topsort :: Graph Node -> Node -> Property
33+
prop_delete_topsort g n =
34+
isRight (topsort g) ==>
35+
isRight (topsort $ deleteNode n g)
36+
37+
prop_op_topsort :: Graph Node -> Property
38+
prop_op_topsort g =
39+
isRight (topsort g) === isRight (topsort $ opGraph g)
40+
41+
prop_trC_topsort :: Graph Node -> Property
42+
prop_trC_topsort g =
43+
within 1_000_000 $ isRight (topsort g) === isRight (topsort $ transitiveClosure g)
44+
45+
prop_trC_opgraph_commute :: Graph Node -> Property
46+
prop_trC_opgraph_commute g =
47+
within 1_000_000 $ transitiveClosure (opGraph g) === opGraph (transitiveClosure g)
48+
49+
prop_depends_grows :: Graph Node -> Graph Node -> Node -> Property
50+
prop_depends_grows g g' n = property $ dependencies n g `Set.isSubsetOf` dependencies n (g <> g')
51+
52+
prop_transitive_dependencies :: Graph Node -> Node -> Property
53+
prop_transitive_dependencies g n =
54+
within 1_000_000 $ transitiveDependencies n g === dependencies n (transitiveClosure g)
55+
56+
prop_topsort_all_nodes :: Graph Node -> Property
57+
prop_topsort_all_nodes g =
58+
case topsort g of
59+
Left{} -> discard
60+
Right o -> Set.fromList o === nodes g
61+
62+
prop_topsort_sound :: Graph Node -> Property
63+
prop_topsort_sound g =
64+
case topsort g of
65+
Left{} -> discard
66+
Right o -> property $ go o
67+
where
68+
go [] = True
69+
go (n : ns) = all (\n' -> not $ dependsOn n n' g) ns && go ns
70+
71+
tests :: Bool -> Spec
72+
tests _nightly =
73+
describe "Graph tests" $ do
74+
prop "prop_no_dependencies_topsort" prop_no_dependencies_topsort
75+
prop "prop_subtract_topsort" prop_subtract_topsort
76+
prop "prop_delete_topsort" prop_delete_topsort
77+
prop "prop_op_topsort" prop_op_topsort
78+
prop "prop_trC_topsort" prop_trC_topsort
79+
prop "prop_trC_opgraph_commute" prop_trC_opgraph_commute
80+
prop "prop_depends_grows" prop_depends_grows
81+
prop "prop_topsort_all_nodes" prop_topsort_all_nodes
82+
prop "prop_topsort_sound" prop_topsort_sound

test/Tests.hs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
module Main where
22

3-
import Constrained.Tests
3+
import Constrained.Tests as Tests
4+
import Constrained.GraphSpec as Graph
45
import Data.Maybe
56
import System.Environment
67
import Test.Hspec
78

89
main :: IO ()
910
main = do
1011
nightly <- isJust <$> lookupEnv "NIGHTLY"
11-
hspec $ tests nightly
12+
hspec $ do
13+
Tests.tests nightly
14+
Graph.tests nightly

0 commit comments

Comments
 (0)