1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
|
{-# LANGUAGE FlexibleContexts, MultiParamTypeClasses #-}
{- |
Module : Data.GraphViz.Testing.Properties
Description : Properties for testing.
Copyright : (c) Ivan Lazar Miljenovic
License : 3-Clause BSD-style
Maintainer : Ivan.Miljenovic@gmail.com
Various properties that should hold true for the graphviz library.
-}
module Data.GraphViz.Testing.Properties where
import Data.GraphViz (dotizeGraph, graphToDot,
nonClusteredParams,
setDirectedness)
import Data.GraphViz.Algorithms
import Data.GraphViz.Internal.Util (groupSortBy, isSingle)
import Data.GraphViz.Parsing (ParseDot (..), parseIt,
parseIt')
import Data.GraphViz.PreProcessing (preProcess)
import Data.GraphViz.Printing (PrintDot (..), printIt)
import Data.GraphViz.Types (DotEdge (..), DotNode (..),
DotRepr (..),
GlobalAttributes (..),
PrintDotRepr,
edgeInformationClean,
graphEdges, graphNodes,
nodeInformationClean,
printDotGraph)
import Data.GraphViz.Types.Canonical (DotGraph (..),
DotStatements (..))
import qualified Data.GraphViz.Types.Generalised as G
import Test.QuickCheck
import Control.Arrow ((&&&))
import Data.Function (on)
import Data.Graph.Inductive (DynGraph, Graph, edges, emap, equal,
labEdges, labNodes, nmap, nodes)
import Data.List (nub, sort)
import qualified Data.Map as Map
import qualified Data.Set as Set
import Data.Text.Lazy (Text)
-- -----------------------------------------------------------------------------
-- The properties to test for
-- | Checking that @parse . print == id@; that is, graphviz can parse
-- its own output.
prop_printParseID :: (ParseDot a, PrintDot a, Eq a) => a -> Bool
prop_printParseID a = tryParse' a == a
-- | A version of 'prop_printParse' specifically for lists; it ensures
-- that the list is not empty (as most list-based parsers fail on
-- empty lists).
prop_printParseListID :: (ParseDot a, PrintDot a, Eq a) => [a] -> Property
prop_printParseListID as = not (null as) ==> prop_printParseID as
-- | When converting a canonical 'DotGraph' value to any other one,
-- they should generate the same Dot code.
prop_generalisedSameDot :: (Ord n, PrintDot n, ParseDot n) => DotGraph n -> Bool
prop_generalisedSameDot dg = printDotGraph dg == printDotGraph gdg
where
gdg = canonicalToType (undefined :: G.DotGraph n) dg
-- | Pre-processing shouldn't change the output of printed Dot code.
-- This should work for all 'PrintDot' instances, but is more
-- specific to 'DotGraph' values.
prop_preProcessingID :: (PrintDotRepr dg n) => dg n -> Bool
prop_preProcessingID dg = preProcess dotCode == dotCode
where
dotCode = printDotGraph dg
-- | This property verifies that 'dotizeGraph', etc. only /augment/ the
-- original graph; that is, the actual nodes, edges and labels for
-- each remain unchanged. Whilst 'dotize', etc. only require
-- 'Graph' instances, this property requires 'DynGraph' (which is a
-- sub-class of 'Graph') instances to be able to strip off the
-- 'Attributes' augmentations.
prop_dotizeAugment :: (DynGraph g, Eq n, Ord e) => g n e -> Bool
prop_dotizeAugment g = equal g (unAugment g')
where
g' = setDirectedness dotizeGraph nonClusteredParams g
unAugment = nmap snd . emap snd
-- | After augmentation, each node and edge should have a non-empty
-- | list of 'Attributes'.
prop_dotizeHasAugment :: (DynGraph g, Ord e) => g n e -> Bool
prop_dotizeHasAugment g = all (not . null) nodeAugments
&& all (not . null) edgeAugments
where
g' = setDirectedness dotizeGraph nonClusteredParams g
nodeAugments = map (fst . snd) $ labNodes g'
edgeAugments = map (fst . \(_,_,l) -> l) $ labEdges g'
-- | When a graph with multiple edges is augmented, then all edges
-- should have unique 'Attributes' (namely the positions). Note
-- that this may not hold true with custom supplied 'Attributes'
-- (i.e. not using one of the @dotize@ functions).
prop_dotizeAugmentUniq :: (DynGraph g, Eq n, Ord e) => g n e -> Bool
prop_dotizeAugmentUniq g = all uniqLs lss
where
g' = setDirectedness dotizeGraph nonClusteredParams g
les = map (\(f,t,l) -> ((f,t),l)) $ labEdges g'
lss = map (map snd) . filter (not . isSingle)
$ groupSortBy fst les
uniqLs [] = False -- Needs to have at least /one/ Attribute!
uniqLs ls = ls == nub ls
-- | Ensure that the definition of 'nodeInformation' for a DotRepr
-- finds all the nodes.
prop_findAllNodes :: (DotRepr dg Int, Ord el, Graph g)
=> dg Int -> g nl el -> Bool
prop_findAllNodes dg' g = ((==) `on` sort) gns dgns
where
gns = nodes g
dg = canonicalToType dg' $ setDirectedness graphToDot nonClusteredParams g
dgns = map nodeID $ graphNodes dg
-- | Ensure that the definition of 'nodeInformation' for DotReprs
-- finds all the nodes when the explicit 'DotNode' definitions are
-- removed.
prop_findAllNodesE :: (DotRepr dg Int, Ord el, Graph g)
=> dg Int -> g nl el -> Bool
prop_findAllNodesE dg' g = ((==) `on` sort) gns dgns
where
gns = nodes g
dg = canonicalToType dg' . removeNodes $ setDirectedness graphToDot nonClusteredParams g
dgns = map nodeID $ graphNodes dg
removeNodes dot@DotGraph{graphStatements = stmts}
= dot { graphStatements
= stmts {nodeStmts = filter notInEdge $ nodeStmts stmts}
}
gnes = Set.fromList . concatMap (\(f,t) -> [f,t]) $ edges g
notInEdge dn = nodeID dn `Set.notMember` gnes
-- | Ensure that the definition of 'edgeInformation' for DotReprs
-- finds all the nodes.
prop_findAllEdges :: (DotRepr dg Int, Ord el, Graph g) => dg Int -> g nl el -> Bool
prop_findAllEdges dg' g = ((==) `on` sort) ges dges
where
ges = edges g
dg = canonicalToType dg' $ graphToDot nonClusteredParams g
dges = map (fromNode &&& toNode) $ graphEdges dg
-- | There should be no clusters or global attributes when converting
-- a 'Graph' to a DotRepr (via fromCanonical) without any formatting
-- or clustering.
prop_noGraphInfo :: (DotRepr dg Int, Ord el, Graph g)
=> dg Int -> g nl el -> Bool
prop_noGraphInfo dg' g = info == (GraphAttrs [], Map.empty)
where
dg = canonicalToType dg'
$ setDirectedness graphToDot nonClusteredParams g
info = graphStructureInformation dg
-- | Canonicalisation should be idempotent.
prop_canonicalise :: (DotRepr dg n) => CanonicaliseOptions -> dg n -> Bool
prop_canonicalise copts g = cdg == canonicaliseOptions copts cdg
where
cdg = canonicaliseOptions copts g
-- | Canonicalisation shouldn't change any nodes.
prop_canonicaliseNodes :: (DotRepr dg n) => CanonicaliseOptions -> dg n -> Bool
prop_canonicaliseNodes copts g = nodeInformationClean True g
== nodeInformationClean True cdg
where
cdg = canonicaliseOptions copts g
-- | Canonicalisation shouldn't change any edges.
prop_canonicaliseEdges :: (DotRepr dg n) => CanonicaliseOptions -> dg n -> Bool
prop_canonicaliseEdges copts g = sort (edgeInformationClean True g)
== sort (edgeInformationClean True cdg)
where
cdg = canonicaliseOptions copts g
-- | Removing transitive edges should be idempotent.
prop_transitive :: (DotRepr dg n) => CanonicaliseOptions -> dg n -> Bool
prop_transitive copts g = tdg == transitiveReductionOptions copts tdg
where
tdg = transitiveReductionOptions copts g
-- | Transitive reduction shouldn't change any nodes.
prop_transitiveNodes :: (DotRepr dg n) => CanonicaliseOptions -> dg n -> Bool
prop_transitiveNodes copts g = nodeInformationClean True g
== nodeInformationClean True cdg
where
cdg = transitiveReductionOptions copts g
-- -----------------------------------------------------------------------------
-- Helper utility functions
-- | A utility function to use for debugging purposes for trying to
-- find how graphviz /is/ parsing something. This is easier than
-- using @'parseIt' . 'printIt'@ directly, since it avoids having to
-- enter and explicit type signature.
tryParse :: (ParseDot a, PrintDot a) => a -> (a, Text)
tryParse = parseIt . printIt
-- | Equivalent to 'tryParse' except that it is assumed that the
-- entire 'String' *is* fully consumed.
tryParse' :: (ParseDot a, PrintDot a) => a -> a
tryParse' = parseIt' . printIt
-- | A wrapper around 'fromCanonical' that lets you specify up-front
-- what type to create (it need not be a sensible value).
canonicalToType :: (DotRepr dg n) => dg n -> DotGraph n -> dg n
canonicalToType _ = fromCanonical
|