on master help sign in
Add new dependency analyzer & tests
commite9666f78e8fd80d4674afb1e519f23d6e414a223 authorJasper Van der Jeugt <jaspervdj@gmail.com> dateMon 28 Mar 2011, 18:35 (16 years ago) parent241efb1
4 files changed+236
src/Hakyll/Core/DependencyAnalyzer.hs+155 src/Hakyll/Core/DirectedGraph.hs+8 tests/Hakyll/Core/DependencyAnalyzer/Tests.hs+70 tests/TestSuite.hs+3
src/Hakyll/Core/DependencyAnalyzer.hsnew file+155
@@ -0,0 +1,155 @@
1+module Hakyll.Core.DependencyAnalyzer
2+ ( DependencyAnalyzer
3+ , Signal (..)
4+ , makeDependencyAnalyzer
5+ , step
6+ , stepAll
7+ ) where
8+
9+import Prelude hiding (reverse)
10+import qualified Prelude as P (reverse)
11+import Control.Arrow (first)
12+import Data.Set (Set)
13+import qualified Data.Set as S
14+import Data.Monoid (Monoid, mappend, mempty)
15+
16+import Hakyll.Core.DirectedGraph
17+
18+-- | This data structure represents the state of the dependency analyzer. It
19+-- holds a complete graph in 'analyzerGraph', which always contains all items,
20+-- whether they are to be compiled or not.
21+--
22+-- The 'analyzerRemains' fields holds the items that still need to be compiled,
23+-- and 'analyzerDone' holds the items which are already compiled. This means
24+-- that initally, 'analyzerDone' is empty and 'analyzerRemains' contains the
25+-- items which are out-of-date (or items which have out-of-date dependencies).
26+--
27+-- We also hold the dependency graph from the previous run because we need it
28+-- when we want to determine when an item is out-of-date. An item is out-of-date
29+-- when:
30+--
31+-- * the resource from which it compiles is out-of-date, or;
32+--
33+-- * any of it's dependencies is out-of-date, or;
34+--
35+-- * it's set of dependencies has changed since the previous run.
36+--
37+data DependencyAnalyzer a = DependencyAnalyzer
38+ { -- | The complete dependency graph
39+ analyzerGraph :: DirectedGraph a
40+ , -- | A set of items yet to be compiled
41+ analyzerRemains :: Set a
42+ , -- | A set of items already compiled
43+ analyzerDone :: Set a
44+ , -- | The dependency graph from the previous run
45+ analyzerPreviousGraph :: DirectedGraph a
46+ } deriving (Show)
47+
48+data Signal a = Build a
49+ | Cycle [a]
50+ | Done
51+
52+instance (Ord a, Show a) => Monoid (DependencyAnalyzer a) where
53+ mempty = DependencyAnalyzer mempty mempty mempty mempty
54+ mappend x y = growRemains $ DependencyAnalyzer
55+ (analyzerGraph x `mappend` analyzerGraph y)
56+ (analyzerRemains x `mappend` analyzerRemains y)
57+ (analyzerDone x `mappend` analyzerDone y)
58+ (analyzerPreviousGraph x `mappend` analyzerPreviousGraph y)
59+
60+-- | Construct a dependency analyzer
61+--
62+makeDependencyAnalyzer :: (Ord a, Show a)
63+ => DirectedGraph a -- ^ The dependency graph
64+ -> (a -> Bool) -- ^ Is an item out-of-date?
65+ -> DirectedGraph a -- ^ The old dependency graph
66+ -> DependencyAnalyzer a -- ^ Resulting analyzer
67+makeDependencyAnalyzer graph isOutOfDate prev =
68+ growRemains $ DependencyAnalyzer graph remains S.empty prev
69+ where
70+ -- Construct the remains set by filtering using the given predicate
71+ remains = S.fromList $ filter isOutOfDate $ map fst $ toList graph
72+
73+-- | The 'analyzerRemains' field of a 'DependencyAnalyzer' is supposed to
74+-- contain all out-of-date items, including the items with out-of-date
75+-- dependencies. However, it is easier to just set up the directly out-of-date
76+-- items initially -- and then grow the remains fields.
77+--
78+-- This function assumes the 'analyzerRemains' fields in incomplete, and tries
79+-- to correct it. Running it when the field is complete has no effect -- but it
80+-- is a pretty expensive function, and it should be used with care.
81+--
82+growRemains :: (Ord a, Show a) => DependencyAnalyzer a -> DependencyAnalyzer a
83+growRemains (DependencyAnalyzer graph remains done prev) =
84+ (DependencyAnalyzer graph remains' done prev)
85+ where
86+ -- Grow the remains set using the indirect and changedDeps values, then
87+ -- filter out the items already done
88+ remains' = S.filter (`S.notMember` done) indirect
89+
90+ -- Select the nodes which are reachable from the remaining nodes in the
91+ -- reversed dependency graph: these are the indirectly out-of-date items
92+ indirect = reachableNodes (remains `S.union` changedDeps) $ reverse graph
93+
94+ -- For all nodes in the graph, check which items have a different dependency
95+ -- set compared to the previous run
96+ changedDeps = S.fromList $ map fst $
97+ filter (uncurry (/=) . first (`neighbours` prev)) $ toList graph
98+
99+-- | Step a dependency analyzer
100+--
101+step :: (Ord a, Show a) => DependencyAnalyzer a -> (Signal a, DependencyAnalyzer a)
102+step analyzer@(DependencyAnalyzer graph remains done prev)
103+ -- No remaining items
104+ | S.null remains = (Done, analyzer)
105+ -- An item remains, let's find a ready item
106+ | otherwise =
107+ let item = S.findMin remains
108+ in case findReady analyzer item of
109+ Done -> (Done, analyzer)
110+ Cycle c -> (Cycle c, analyzer)
111+ -- A ready item was found, signal a build
112+ Build build ->
113+ let remains' = S.delete build remains
114+ done' = S.insert build done
115+ in (Build build, DependencyAnalyzer graph remains' done' prev)
116+
117+-- | Step until done, creating a set of items we need to build -- mostly used
118+-- for debugging purposes
119+--
120+stepAll :: (Ord a, Show a) => DependencyAnalyzer a -> Maybe (Set a)
121+stepAll = stepAll' S.empty
122+ where
123+ stepAll' xs analyzer = case step analyzer of
124+ (Build x, analyzer') -> stepAll' (S.insert x xs) analyzer'
125+ (Done, _) -> Just xs
126+ (Cycle _, _) -> Nothing
127+
128+-- | Find an item ready to be compiled
129+--
130+findReady :: (Ord a, Show a) => DependencyAnalyzer a -> a -> Signal a
131+findReady analyzer = findReady' [] S.empty
132+ where
133+ -- The dependency graph
134+ graph = analyzerGraph analyzer
135+
136+ -- Items to do
137+ todo = analyzerRemains analyzer `S.difference` analyzerDone analyzer
138+
139+ -- Worker
140+ findReady' stack visited item
141+ -- We already visited this item, the cycle is the reversed stack
142+ | item `S.member` visited = Cycle $ P.reverse stack'
143+ -- Look at the neighbours we to do
144+ | otherwise = case filter (`S.member` todo) neighbours' of
145+ -- No neighbours available to be done: it's ready!
146+ [] -> Build item
147+ -- At least one neighbour is available, search for that one
148+ (x : _) -> findReady' stack' visited' x
149+ where
150+ -- Our neighbours
151+ neighbours' = S.toList $ neighbours item graph
152+
153+ -- The new visited stack/set
154+ stack' = item : stack
155+ visited' = S.insert item visited
src/Hakyll/Core/DirectedGraph.hs+8
@@ -4,6 +4,7 @@
44module Hakyll.Core.DirectedGraph
55 ( DirectedGraph
66 , fromList
7+ , toList
78 , member
89 , nodes
910 , neighbours
@@ -13,6 +14,7 @@ module Hakyll.Core.DirectedGraph
1314 ) where
1415
1516import Prelude hiding (reverse)
17+import Control.Arrow (second)
1618import Data.Monoid (mconcat)
1719import Data.Set (Set)
1820import Data.Maybe (fromMaybe)
@@ -28,6 +30,12 @@ fromList :: Ord a
2830 -> DirectedGraph a -- ^ Resulting directed graph
2931fromList = DirectedGraph . M.fromList . map (\(t, d) -> (t, Node t d))
3032
33+-- | Deconstruction of directed graphs
34+--
35+toList :: DirectedGraph a
36+ -> [(a, Set a)]
37+toList = map (second nodeNeighbours) . M.toList . unDirectedGraph
38+
3139-- | Check if a node lies in the given graph
3240--
3341member :: Ord a
tests/Hakyll/Core/DependencyAnalyzer/Tests.hsnew file+70
@@ -0,0 +1,70 @@
1+module Hakyll.Core.DependencyAnalyzer.Tests where
2+
3+import Control.Arrow (second)
4+import qualified Data.Set as S
5+import Data.Monoid (mempty)
6+
7+import Test.Framework
8+import Test.Framework.Providers.HUnit
9+import Test.HUnit hiding (Test)
10+
11+import Hakyll.Core.DirectedGraph
12+import Hakyll.Core.DependencyAnalyzer
13+
14+tests :: [Test]
15+tests =
16+ [ testCase "step [1]" step1
17+ , testCase "step [2]" step2
18+ ]
19+
20+step1 :: Assertion
21+step1 = Just (S.fromList [1, 2, 5, 6, 7, 8, 9]) @?=
22+ stepAll (makeDependencyAnalyzer graph isOutOfDate prev)
23+ where
24+ node = curry $ second S.fromList
25+
26+ graph = fromList
27+ [ node (8 :: Int) [2, 4, 6]
28+ , node 2 [4, 3]
29+ , node 4 [3]
30+ , node 6 [4]
31+ , node 3 []
32+ , node 9 [5]
33+ , node 5 [7]
34+ , node 1 [7]
35+ , node 7 []
36+ ]
37+
38+ prev = fromList
39+ [ node 8 [2, 4, 6]
40+ , node 2 [4, 3]
41+ , node 4 [3]
42+ , node 6 [4]
43+ , node 3 []
44+ , node 9 [5]
45+ , node 5 [7]
46+ , node 1 [7]
47+ , node 7 [8]
48+ ]
49+
50+ isOutOfDate = (`elem` [5, 2, 6])
51+
52+step2 :: Assertion
53+step2 = Nothing @?= stepAll (makeDependencyAnalyzer graph isOutOfDate mempty)
54+ where
55+ node = curry $ second S.fromList
56+
57+ -- Cycle: 4 -> 7 -> 5 -> 9 -> 4
58+ graph = fromList
59+ [ node (1 :: Int) [6]
60+ , node 2 [3]
61+ , node 3 []
62+ , node 4 [1, 7, 8]
63+ , node 5 [9]
64+ , node 6 [3]
65+ , node 7 [5]
66+ , node 8 [2]
67+ , node 9 [4]
68+ ]
69+
70+ isOutOfDate = const True
tests/TestSuite.hs+3
@@ -3,6 +3,7 @@ module TestSuite where
33import Test.Framework (defaultMain, testGroup)
44
55import qualified Hakyll.Core.DirectedGraph.Tests
6+import qualified Hakyll.Core.DependencyAnalyzer.Tests
67import qualified Hakyll.Core.Identifier.Tests
78import qualified Hakyll.Core.Routes.Tests
89import qualified Hakyll.Web.Page.Tests
@@ -14,6 +15,8 @@ main :: IO ()
1415main = defaultMain
1516 [ testGroup "Hakyll.Core.DirectedGraph.Tests"
1617 Hakyll.Core.DirectedGraph.Tests.tests
18+ , testGroup "Hakyll.Core.DependencyAnalyzer.Tests"
19+ Hakyll.Core.DependencyAnalyzer.Tests.tests
1720 , testGroup "Hakyll.Core.Identifier.Tests"
1821 Hakyll.Core.Identifier.Tests.tests
1922 , testGroup "Hakyll.Core.Routes.Tests"