-- This file is part of Goatee. -- -- Copyright 2014-2015 Bryan Gardiner -- -- Goatee is free software: you can redistribute it and/or modify -- it under the terms of the GNU Affero General Public License as published by -- the Free Software Foundation, either version 3 of the License, or -- (at your option) any later version. -- -- Goatee is distributed in the hope that it will be useful, -- but WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -- GNU Affero General Public License for more details. -- -- You should have received a copy of the GNU Affero General Public License -- along with Goatee. If not, see . {-# LANGUAGE CPP, FlexibleContexts #-} module Game.Goatee.Lib.MonadTest (tests) where #if !MIN_VERSION_base(4,8,0) import Control.Applicative ((<$>)) #endif import Control.Arrow ((&&&), second) import Control.Monad (forM_, liftM, replicateM_, void) import Control.Monad.Writer (Writer, execWriter, runWriter, tell) import Data.List (sortBy, unfoldr) import qualified Data.Map as Map import Data.Maybe (fromJust, maybeToList) #if !MIN_VERSION_base(4,8,0) import Data.Monoid (Monoid) #endif import Data.Ord (comparing) import Game.Goatee.Common import Game.Goatee.Lib.Board import Game.Goatee.Lib.Monad import Game.Goatee.Lib.Property import Game.Goatee.Lib.TestInstances () import Game.Goatee.Lib.TestUtils import Game.Goatee.Lib.Tree (emptyNode, nodeChildren) import Game.Goatee.Lib.Types import Game.Goatee.Test.Common import Test.HUnit ((~:), (@=?), (@?=), Assertion, Test (TestList), assertFailure) {-# ANN module "HLint: ignore Reduce duplication" #-} type LoggedGoM = GoT (Writer [String]) runLoggedGo :: LoggedGoM a -> Cursor -> (a, Cursor, [String]) runLoggedGo go cursor = let ((value, cursor'), log) = runWriter $ runGoT go cursor in (value, cursor', log) tests = "Game.Goatee.Lib.Monad" ~: TestList [ monadTests , navigationTests , positionStackTests , propertiesTests , getPropertyTests , getPropertyValueTests , putPropertyTests , deletePropertyTests , modifyPropertyTests , modifyPropertyValueTests , modifyPropertyStringTests , modifyPropertyListTests , modifyPropertyCoordsTests , modifyGameInfoTests , modifyVariationModeTests , getAssignedStoneTests , getAllAssignedStonesTests , modifyAssignedStonesTests , getMarkTests , modifyMarkTests , addChildTests , addChildAtTests , deleteChildAtTests , gameInfoChangedTests ] monadTests = "monad properties" ~: TestList [ "returns a value it's given" ~: let cursor = rootCursor $ node [] (value, cursor') = runGo (return 3) cursor in (value, cursorNode cursor') @?= (3, cursorNode cursor) , "getCursor works" ~: do let cursor = rootCursor nodeA nodeA = node1 [SZ 3 3, B $ Just (0,0)] nodeB nodeB = node1 [W $ Just (1,1)] nodeC nodeC = node [B $ Just (2,2)] nodeA @=? evalGo (liftM cursorNode getCursor) cursor nodeB @=? evalGo (goDown 0 >> liftM cursorNode getCursor) cursor nodeC @=? evalGo (goDown 0 >> goDown 0 >> liftM cursorNode getCursor) cursor , "getCoordState works" ~: do let cursor = child 0 $ child 0 $ rootCursor $ node1 [SZ 2 2, B $ Just (0,0)] $ node1 [W $ Just (1,0), TR $ coord1 (0,0)] $ node [B $ Just (0,1), CR $ coord1 (1,0)] action = mapM getCoordState [(0,0), (1,0), (0,1), (1,1)] states = evalGo action cursor map coordStone states @?= [Just Black, Just White, Just Black, Nothing] map coordMark states @?= [Nothing, Just MarkCircle, Nothing, Nothing] ] navigationTests = "navigation" ~: TestList [ "navigates down a tree" ~: do let cursor = rootCursor $ node1 [B $ Just (0,0)] $ node' [W $ Just (1,1)] [node [B $ Just (2,2)], node [B Nothing]] action = do tell . (:[]) =<< goDown 0 tell . (:[]) =<< goDown 1 tell . (:[]) =<< goDown 0 ((_, cursor'), log) = runWriter $ runGoT action cursor cursorProperties cursor' @?= [B Nothing] log @?= [True, True, False] , "navigates up a tree" ~: do let cursor = child 1 $ child 0 $ rootCursor $ node1 [B $ Just (0,0)] $ node' [W $ Just (1,1)] [node [B $ Just (2,2)], node [B Nothing]] action = replicateM_ 3 (tell . (:[]) =<< goUp) ((_, cursor'), log) = runWriter $ runGoT action cursor cursorProperties cursor' @?= [B $ Just (0,0)] log @?= [True, True, False] , "navigates to the left" ~: let cursor = child 2 $ child 0 $ rootCursor $ node1 [B $ Just (0,0)] $ node' [W $ Just (1,1)] [node [B $ Just (2,2)], node [B $ Just (3,3)], node [B $ Just (4,4)]] action = do tell . (:[]) . show =<< goLeft tell . (:[]) . show =<< getProperties tell . (:[]) . show =<< goLeft tell . (:[]) . show =<< getProperties tell . (:[]) . show =<< goLeft tell . (:[]) . show =<< getProperties log = execWriter $ runGoT action cursor in log @?= ["True", "[B (Just (3,3))]", "True", "[B (Just (2,2))]", "False", "[B (Just (2,2))]"] , "navigates to the right" ~: let cursor = child 0 $ child 0 $ rootCursor $ node1 [B $ Just (0,0)] $ node' [W $ Just (1,1)] [node [B $ Just (2,2)], node [B $ Just (3,3)], node [B $ Just (4,4)]] action = do tell . (:[]) . show =<< goRight tell . (:[]) . show =<< getProperties tell . (:[]) . show =<< goRight tell . (:[]) . show =<< getProperties tell . (:[]) . show =<< goRight tell . (:[]) . show =<< getProperties log = execWriter $ runGoT action cursor in log @?= ["True", "[B (Just (3,3))]", "True", "[B (Just (4,4))]", "False", "[B (Just (4,4))]"] , "invokes handlers when navigating" ~: let cursor = rootCursor $ node1 [B Nothing] $ node [W Nothing] action = do on navigationEvent $ \step -> case step of GoUp index -> tell ["Up " ++ show index] _ -> return () on navigationEvent $ \step -> case step of GoDown index -> tell ["Down " ++ show index] _ -> return () True <- goDown 0 True <- goUp return () (_, _, log) = runLoggedGo action cursor in log @?= ["Down 0", "Up 0"] , "navigates to the root of a tree, invoking handlers" ~: do let cursor = child 0 $ child 0 $ rootCursor $ node1 [B $ Just (0,0)] $ node1 [W $ Just (1,1)] $ node [B $ Just (2,2)] action = do on navigationEvent $ \step -> tell [show step] goToRoot (_, cursor', log) = runLoggedGo action cursor cursorProperties cursor' @?= [B $ Just (0,0)] log @?= ["GoUp 0", "GoUp 0"] , "goToGameInfoNode" ~: TestList [ "can navigate up to find game info" ~: let props = [PB (toSimpleText ""), B Nothing] cursor = child 0 $ rootCursor $ node1 props $ node [W Nothing] action = void $ goToGameInfoNode False in cursorProperties (execGo action cursor) @?= props , "can find game info at the current node" ~: let props = [PB (toSimpleText ""), W Nothing] cursor = child 0 $ rootCursor $ node1 [B Nothing] $ node props action = void $ goToGameInfoNode False in cursorProperties (execGo action cursor) @?= props , "doesn't find game info from a subnode" ~: let cursor = child 0 $ rootCursor $ node1 [B $ Just (0,0)] $ node1 [W $ Just (1,1)] $ node [PB (toSimpleText ""), B Nothing] action = void $ goToGameInfoNode False in cursorProperties (execGo action cursor) @?= [W $ Just (1,1)] , "can return to the initial node when no game info is found" ~: let cursor = child 0 $ child 0 $ rootCursor $ node1 [B $ Just (0,0)] $ node1 [W $ Just (1,1)] $ node [B $ Just (2,2)] action = void $ goToGameInfoNode False in cursorProperties (execGo action cursor) @?= [B $ Just (2,2)] , "can finish at the root node when no game info is found" ~: let cursor = child 0 $ child 0 $ rootCursor $ node1 [B $ Just (0,0)] $ node1 [W $ Just (1,1)] $ node [B $ Just (2,2)] action = void $ goToGameInfoNode True in cursorProperties (execGo action cursor) @?= [B $ Just (0,0)] ] ] positionStackTests = "position stack" ~: TestList [ "should push, pop, and drop with no navigation" ~: do let cursor = rootCursor $ node [] actions = [pushPosition >> popPosition, pushPosition >> pushPosition >> popPosition >> popPosition, pushPosition >> dropPosition, pushPosition >> pushPosition >> dropPosition >> popPosition, pushPosition >> pushPosition >> popPosition >> dropPosition] forM_ actions $ \action -> cursorProperties (execGo action cursor) @?= [] , "should backtrack up and down the tree" ~: do let cursor = child 0 $ child 1 $ rootCursor $ node' [B $ Just (0,0)] [node1 [W $ Just (1,1)] $ node [B $ Just (2,2)], node1 [W Nothing] $ node [B Nothing]] action = pushPosition >> goUp >> goUp >> goDown 0 >> goDown 0 cursorProperties (execGo (action >> popPosition) cursor) @?= [B Nothing] cursorProperties (execGo (action >> dropPosition) cursor) @?= [B $ Just (2,2)] , "should pop multiple stacks" ~: do let cursor = child 0 $ child 0 commonCursor action = do navigate log >> popPosition log >> popPosition log execWriter (runGoT action cursor) @?= ["B (2,2)", "B (3,3)", "B (5,5)", "B (3,3)", "B (2,2)"] , "should drop then pop" ~: do let cursor = child 0 $ child 0 commonCursor action = do navigate log >> dropPosition log >> popPosition log execWriter (runGoT action cursor) @?= ["B (2,2)", "B (3,3)", "B (5,5)", "B (5,5)", "B (2,2)"] , "should drop twice" ~: do let cursor = child 0 $ child 0 commonCursor action = do navigate log >> dropPosition log >> dropPosition log execWriter (runGoT action cursor) @?= ["B (2,2)", "B (3,3)", "B (5,5)", "B (5,5)", "B (5,5)"] , "should fire navigation handlers while popping" ~: do let cursor = rootCursor $ node1 [B Nothing] $ node [W Nothing] action = do pushPosition True <- goDown 0 True <- goUp on navigationEvent $ \step -> tell [step] popPosition execWriter (runGoT action cursor) @?= [GoDown 0, GoUp 0] ] where commonCursor = rootCursor $ node' [B $ Just (0,0)] [node' [W $ Just (1,1)] [node [B $ Just (2,2)], node [B $ Just (3,3)]], node1 [W $ Just (4,4)] $ node [B $ Just (5,5)]] log = getCursor >>= \cursor -> case cursorProperties cursor of [B (Just x)] -> tell ["B " ++ show x] [W (Just x)] -> tell ["W " ++ show x] xs -> error $ "Unexpected properties: " ++ show xs navigate = do log >> pushPosition True <- goUp True <- goDown 1 log >> pushPosition True <- goUp True <- goUp True <- goDown 1 True <- goDown 0 return () propertiesTests = "properties" ~: TestList [ "getProperties" ~: TestList [ "returns an empty list" ~: let cursor = rootCursor $ node [] in evalGo getProperties cursor @?= [] , "returns a non-empty list" ~: let properties = [PB $ toSimpleText "Foo", B Nothing] cursor = rootCursor $ node properties in evalGo getProperties cursor @?= properties ] , "modifyProperties" ~: TestList [ "adds properties" ~: let cursor = rootCursor $ node [FF 1] action = do modifyProperties $ \props -> props ++ [B Nothing, W Nothing] getProperties in evalGo action cursor @?= [FF 1, B Nothing, W Nothing] , "removes properties" ~: let cursor = rootCursor $ node [W Nothing, FF 1, B Nothing] action = do modifyProperties $ \props -> filter (not . isMoveProperty) props getProperties in evalGo action cursor @?= [FF 1] , "fires a properties modified event" ~: let cursor = rootCursor $ node [FF 1] action = do on propertiesModifiedEvent $ \old new -> tell [(old, new)] modifyProperties $ const [FF 2] log = execWriter (runGoT action cursor) in log @?= [([FF 1], [FF 2])] ] ] where isMoveProperty prop = case prop of B _ -> True W _ -> True _ -> False getPropertyTests = "getProperty" ~: TestList [ "doesn't find an unset property" ~: do Nothing @=? evalGo (getProperty propertyW) (rootCursor $ node []) Nothing @=? evalGo (getProperty propertyW) (rootCursor $ node [B Nothing]) , "finds a set property" ~: do Just (B Nothing) @=? evalGo (getProperty propertyB) (rootCursor $ node [B Nothing]) Just (B Nothing) @=? evalGo (getProperty propertyB) (rootCursor $ node [B Nothing, DO]) Just DO @=? evalGo (getProperty propertyDO) (rootCursor $ node [B Nothing, DO]) ] getPropertyValueTests = "getPropertyValue" ~: TestList [ "doesn't find an unset property" ~: do Nothing @=? evalGo (getPropertyValue propertyW) (rootCursor $ node []) Nothing @=? evalGo (getPropertyValue propertyW) (rootCursor $ node [B Nothing]) , "finds a set property" ~: do Just Nothing @=? evalGo (getPropertyValue propertyB) (rootCursor $ node [B Nothing]) Just (Just (0,0)) @=? evalGo (getPropertyValue propertyB) (rootCursor $ node [B $ Just (0,0), TE Double1]) Just Double1 @=? evalGo (getPropertyValue propertyTE) (rootCursor $ node [B $ Just (0,0), TE Double1]) ] putPropertyTests = "putProperty" ~: TestList [ "adds an unset property" ~: do [IT] @=? cursorProperties (execGo (putProperty IT) $ rootCursor $ node []) [DO, IT] @=? sortProperties (cursorProperties (execGo (putProperty IT) $ rootCursor $ node [DO])) , "replaces an existing property" ~: [TE Double2] @=? cursorProperties (execGo (putProperty $ TE Double2) (rootCursor $ node [TE Double1])) ] deletePropertyTests = "deleteProperty" ~: TestList [ "does nothing when the property isn't set" ~: do [] @=? cursorProperties (execGo (deleteProperty DO) $ rootCursor $ node []) [B Nothing] @=? cursorProperties (execGo (deleteProperty DO) $ rootCursor $ node [B Nothing]) , "removes a property" ~: do [] @=? cursorProperties (execGo (deleteProperty DO) $ rootCursor $ node [DO]) -- This is documented behaviour for deleteProperty, the fact that it doesn't -- matter what the property value is: [DO] @=? cursorProperties (execGo (deleteProperty $ B $ Just (0,0)) $ rootCursor $ node [DO, B Nothing]) ] modifyPropertyTests = "modifyProperty" ~: TestList [ "adds a property to an empty node" ~: let cursor = rootCursor $ node [] action = modifyProperty propertyIT (\Nothing -> Just IT) in cursorProperties (execGo action cursor) @?= [IT] , "adds a property to a non-empty node" ~: let cursor = rootCursor $ node [KO] action = modifyProperty propertyIT (\Nothing -> Just IT) in sortProperties (cursorProperties $ execGo action cursor) @?= [IT, KO] , "removes a property" ~: do let cursor = rootCursor $ node [IT, KO] cursor' = execGo (modifyProperty propertyKO $ \(Just KO) -> Nothing) cursor cursor'' = execGo (modifyProperty propertyIT $ \(Just IT) -> Nothing) cursor' cursorProperties cursor' @?= [IT] cursorProperties cursor'' @?= [] , "updates a property" ~: do let cursor = rootCursor $ node [B $ Just (0,0), BM Double2] action = modifyProperty propertyB $ \(Just (B (Just (0,0)))) -> Just $ B $ Just (1,1) cursor' = execGo action cursor action' = modifyProperty propertyBM $ \(Just (BM Double2)) -> Just $ BM Double1 cursor'' = execGo action' cursor' sortProperties (cursorProperties cursor') @?= [B $ Just (1,1), BM Double2] sortProperties (cursorProperties cursor'') @?= [B $ Just (1,1), BM Double1] , "fires an event when modifying a property" ~: do let cursor = rootCursor $ node [B $ Just (0,0)] action = do on propertiesModifiedEvent $ \[B (Just (0,0))] [] -> tell [0] modifyProperty propertyB $ \(Just (B (Just (0,0)))) -> Nothing (cursor', log) = runWriter (execGoT action cursor) [0] @=? log [] @=? cursorProperties cursor' , "modifying but not actually changing a property doesn't fire an event" ~: do let cursor = rootCursor $ node [B $ Just (0,0)] action = do on propertiesModifiedEvent $ \_ _ -> tell ["Event fired."] modifyProperty propertyB $ \p@(Just (B {})) -> p (cursor', log) = runWriter (execGoT action cursor) [B $ Just (0,0)] @=? cursorProperties cursor' [] @=? log ] modifyPropertyValueTests = "modifyPropertyValue" ~: TestList [ "adds a property to an empty node" ~: let cursor = rootCursor $ node [] action = modifyPropertyValue propertyPL (\Nothing -> Just Black) in cursorProperties (execGo action cursor) @?= [PL Black] , "adds a property to a non-empty node" ~: let cursor = rootCursor $ node [KO] action = modifyPropertyValue propertyPL (\Nothing -> Just White) in sortProperties (cursorProperties $ execGo action cursor) @?= [KO, PL White] , "removes a property" ~: do let cursor = rootCursor $ node [B Nothing, TE Double2] cursor' = execGo (modifyPropertyValue propertyTE $ \(Just Double2) -> Nothing) cursor cursor'' = execGo (modifyPropertyValue propertyB $ \(Just Nothing) -> Nothing) cursor' cursorProperties cursor' @?= [B Nothing] cursorProperties cursor'' @?= [] , "updates a property" ~: do let cursor = rootCursor $ node [B $ Just (0,0), BM Double2] action = modifyPropertyValue propertyB $ \(Just (Just (0,0))) -> Just $ Just (1,1) cursor' = execGo action cursor action' = modifyPropertyValue propertyBM $ \(Just Double2) -> Just Double1 cursor'' = execGo action' cursor' sortProperties (cursorProperties cursor') @?= [B $ Just (1,1), BM Double2] sortProperties (cursorProperties cursor'') @?= [B $ Just (1,1), BM Double1] ] modifyPropertyStringTests = "modifyPropertyString" ~: TestList $ -- Test a Text property and a SimpleText property. assumptions ++ genTests "comment" propertyC ++ genTests "game name" propertyGN where assumptions = let _ = C $ toText "" _ = GN $ toSimpleText "" in [] genTests name property = [ "adds a " ++ name ~: let cursor = rootCursor $ node [] action = modifyPropertyString property (++ "Hello.") in cursorProperties (execGo action cursor) @?= [propertyBuilder property $ stringToSgf "Hello."] , "removes a " ++ name ~: let cursor = rootCursor $ node [propertyBuilder property $ stringToSgf "Hello."] action = modifyPropertyString property $ \value -> case value of "Hello." -> "" other -> error $ "Got: " ++ other in cursorProperties (execGo action cursor) @?= [] , "updates a " ++ name ~: let cursor = child 0 $ rootCursor $ node1 [propertyBuilder property $ stringToSgf "one"] $ node [propertyBuilder property $ stringToSgf "two"] action = modifyPropertyString property $ \value -> case value of "two" -> "three" other -> error $ "Got: " ++ other cursor' = execGo action cursor in (cursorProperties cursor', cursorProperties $ fromJust $ cursorParent cursor') @?= ([propertyBuilder property $ stringToSgf "three"], [propertyBuilder property $ stringToSgf "one"]) , "leaves a non-existant comment" ~: let result = execGo (modifyPropertyString property id) $ rootCursor $ node [] in cursorProperties result @?= [] ] modifyPropertyListTests = "modifyPropertyList" ~: TestList [ "adds a property where there was none" ~: let cursor = rootCursor $ node [] arrows = [((0,0), (1,1)), ((0,1), (1,0))] action = modifyPropertyList propertyAR $ \[] -> arrows in [AR arrows] @=? cursorProperties (execGo action cursor) , "removes a property where there was one" ~: let lines = [Line (0,0) (0,1), Line (1,0) (1,1)] cursor = rootCursor $ node [LN lines] action = modifyPropertyList propertyLN $ \old -> if old == lines then [] else error $ "Unexpected old value: " ++ show old in [] @=? cursorProperties (execGo action cursor) , "modifies an existing property" ~: let lines = [Line (0,0) (0,1), Line (1,0) (1,1)] lines' = [head lines] cursor = rootCursor $ node [LN lines] action = modifyPropertyList propertyLN $ \old -> if old == lines then lines' else error $ "Unexpected old value: " ++ show old in [LN lines'] @=? cursorProperties (execGo action cursor) , "fires propertiesModifiedEvent when changed" ~: let lines = [Line (0,0) (0,1), Line (1,0) (1,1)] lines' = [head lines] cursor = rootCursor $ node [LN lines] action = do on propertiesModifiedEvent $ \old new -> tell [(old, new)] modifyPropertyList propertyLN $ \old -> if old == lines then lines' else error $ "Unexpected old value: " ++ show old in [([LN lines], [LN lines'])] @=? execWriter (runGoT action cursor) , "doesn't fire propertiesModifiedEvent when not changed" ~: let lines = [Line (0,0) (0,1), Line (1,0) (1,1)] cursor = rootCursor $ node [LN lines] action = do on propertiesModifiedEvent $ \old new -> tell [(old, new)] modifyPropertyList propertyLN id in [] @=? execWriter (runGoT action cursor) ] modifyPropertyCoordsTests = "modifyPropertyCoords" ~: TestList [ "adds a property where there was none" ~: let cursor = rootCursor $ node [] action = modifyPropertyCoords propertySL $ \[] -> [(0,0), (1,1)] in [SL $ coords [(0,0), (1,1)]] @=? cursorProperties (execGo action cursor) , "removes a property where there was one" ~: let cursor = rootCursor $ node [TR $ coord1 (5,5)] action = modifyPropertyCoords propertyTR $ \[(5,5)] -> [] in [] @=? cursorProperties (execGo action cursor) , "modifies an existing property" ~: let cursor = rootCursor $ node [CR $ coord1 (3,4)] action = modifyPropertyCoords propertyCR $ \[(3,4)] -> [(2,2)] in [CR $ coord1 (2,2)] @=? cursorProperties (execGo action cursor) , "doesn't affect other properties" ~: let cursor = rootCursor $ node [SQ $ coord1 (0,0), MA $ coord1 (1,1)] action = modifyPropertyCoords propertyMA $ \[(1,1)] -> [(2,2)] in [MA $ coord1 (2,2), SQ $ coord1 (0,0)] @=? sortProperties (cursorProperties $ execGo action cursor) ] modifyGameInfoTests = "modifyGameInfo" ~: TestList [ "creates info on the root node, starting with none" ~: TestList [ "starting from the root node" ~: let cursor = rootCursor $ node [] action = modifyGameInfo $ \info -> info { gameInfoGameName = Just $ toSimpleText "Orange" } in cursorProperties (execGo action cursor) @?= [GN $ toSimpleText "Orange"] , "starting from a non-root node" ~: let cursor = child 0 $ rootCursor $ node1 [] $ node [B Nothing] action = modifyGameInfo $ \info -> info { gameInfoGameName = Just $ toSimpleText "Orange" } cursor' = execGo action cursor in (cursorProperties cursor', cursorProperties $ fromJust $ cursorParent cursor') @?= ([B Nothing], [GN $ toSimpleText "Orange"]) ] , "modifies existing info on the root" ~: TestList [ "starting from the root node" ~: let cursor = rootCursor $ node [GN $ toSimpleText "Orange"] action = modifyGameInfo (\info -> info { gameInfoGameName = Nothing , gameInfoBlackName = Just $ toSimpleText "Peanut butter" }) in cursorProperties (execGo action cursor) @?= [PB $ toSimpleText "Peanut butter"] , "starting from a non-root node" ~: let cursor = child 0 $ rootCursor $ node1 [GN $ toSimpleText "Orange"] $ node [B Nothing] action = modifyGameInfo (\info -> info { gameInfoGameName = Nothing , gameInfoBlackName = Just $ toSimpleText "Peanut butter" }) cursor' = execGo action cursor in (cursorProperties cursor', cursorProperties $ fromJust $ cursorParent cursor') @?= ([B Nothing], [PB $ toSimpleText "Peanut butter"]) ] , "moves game info from a non-root node to the root" ~: let cursor = child 0 $ child 0 $ child 0 $ rootCursor $ node1 [SZ 19 19] $ node1 [B $ Just (0,0), GN $ toSimpleText "Orange"] $ node1 [W $ Just (1,1)] $ node [B $ Just (2,2)] action = modifyGameInfo (\info -> info { gameInfoGameName = Nothing }) cursor' = execGo action cursor action' = modifyGameInfo (\info -> info { gameInfoBlackName = Just $ toSimpleText "Peanut butter" }) cursor'' = execGo action' cursor' -- unfoldr :: (Maybe Cursor -> Maybe ([Property], Maybe Cursor)) -- -> Maybe Cursor -- -> [[Property]] properties = unfoldr (fmap $ cursorProperties &&& cursorParent) (Just cursor'') in map sortProperties properties @?= [[B $ Just (2,2)], [W $ Just (1,1)], [B $ Just (0,0)], [PB $ toSimpleText "Peanut butter", SZ 19 19]] ] modifyVariationModeTests = "modifyVariationMode" ~: TestList [ "testing modes are not default" ~: do defaultVariationMode @/=? mode defaultVariationMode @/=? mode2 , "modifies when at a root node" ~: node1 [ST mode] (node []) @=? cursorNode (execGo setMode $ rootCursor $ node1 [] $ node []) , "modifies when at a non-root node" ~: node1 [ST mode] (node []) @=? cursorNode (cursorRoot $ execGo setMode $ child 0 $ rootCursor $ node1 [] $ node []) , "leaves an unset ST unset" ~: assertST Nothing Nothing $ const defaultVariationMode , "leaves a default ST alone" ~: assertST (Just defaultVariationMode) (Just defaultVariationMode) $ const defaultVariationMode , "leaves an existing ST alone" ~: assertST (Just mode) (Just mode) $ const mode , "adds an ST property" ~: assertST (Just mode) Nothing $ const mode , "removes an ST property when setting to default" ~: assertST Nothing (Just mode) $ const defaultVariationMode , "modifies an existing ST property" ~: assertST (Just mode2) (Just mode) $ const mode2 ] where mode = VariationMode ShowCurrentVariations True mode2 = VariationMode ShowChildVariations False setMode = modifyVariationMode $ const mode assertST maybeExpectedST maybeInitialST fn = node (ST <$> maybeToList maybeExpectedST) @=? cursorNode (execGo (modifyVariationMode fn) $ rootCursor $ node $ ST <$> maybeToList maybeInitialST) getAssignedStoneTests = "getAssignedStone" ~: TestList [ "should return Nothing for a point without an assignment" ~: evalGo (getAssignedStone (0,0)) cursor @?= Nothing , "should return Nothing for an assignment in a parent node" ~: evalGo (getAssignedStone (1,0)) cursor @?= Nothing , "should return Just for assigned points" ~: do evalGo (getAssignedStone (2,0)) cursor @?= Just (Just Black) evalGo (getAssignedStone (3,0)) cursor @?= Just (Just White) evalGo (getAssignedStone (4,0)) cursor @?= Just Nothing ] where cursor = child 0 $ rootCursor $ root 5 1 [AB $ coord1 (1,0), AW $ coord1 (4,0)] [node [AB $ coord1 (2,0), AW $ coord1 (3,0), AE $ coord1 (4,0)]] getAllAssignedStonesTests = "getAllAssignedStones" ~: TestList [ "returns empty given no stone assignments" ~: do Map.empty @=? evalGo getAllAssignedStones (rootCursor $ node []) Map.empty @=? evalGo getAllAssignedStones (rootCursor $ node [B $ Just (0,0)]) , "ignores stone assignments in a parent node" ~: Map.empty @=? evalGo getAllAssignedStones (child 0 $ rootCursor $ root 1 1 [AB $ coord1 (0,0)] [node []]) , "finds a single stone assignment" ~: Map.fromList [((0,0), Just Black)] @=? evalGo getAllAssignedStones (rootCursor $ root 1 1 [AB $ coord1 (0,0)] []) , "finds multiple stone assignments" ~: Map.fromList [((0,0), Just Black), ((1,0), Just Black), ((3,0), Just White), ((4,0), Nothing), ((5,0), Just Black)] @=? evalGo getAllAssignedStones (rootCursor $ root 6 1 [AB $ buildCoordList [(0,0), (1,0), (5,0)], AW $ coord1 (3,0), AE $ coord1 (4,0)] []) ] modifyAssignedStonesTests = "modifyAssignedStones" ~: TestList [ "adds assignments to a node without" ~: forM_ [Nothing, Just Black, Just White] $ \stone -> test (rootCursor $ root 2 2 [] []) (do modifyAssignedStones [(1,1)] $ const $ Just stone getAllAssignedStones) (Map.fromList [((1,1), stone)]) noLogAssertion , "adds assignments to a node with existing assignments of the same type" ~: test (rootCursor $ root 2 2 [AE $ coord1 (0,0)] []) (do modifyAssignedStones [(0,1), (1,1)] $ const $ Just Nothing getAllAssignedStones) (Map.fromList [((0,0), Nothing), ((0,1), Nothing), ((1,1), Nothing)]) noLogAssertion , "adds assignments to a node with existing assignments of a different type" ~: test (rootCursor $ root 2 2 [AE $ coord1 (0,0)] []) (do modifyAssignedStones [(0,1), (1,1)] $ const $ Just $ Just White getAllAssignedStones) (Map.fromList [((0,0), Nothing), ((0,1), Just White), ((1,1), Just White)]) noLogAssertion , "assigning overwrites overlapping assignments of a different type" ~: test (rootCursor $ root 2 2 [AE $ coord1 (0,0), AB $ coord1 (0,1), AW $ coord1 (1,1)] []) (do modifyAssignedStones [(0,0), (0,1), (1,0), (1,1)] $ const $ Just $ Just White getAllAssignedStones) (Map.fromList $ (\coord -> (coord, Just White)) <$> [(0,0), (0,1), (1,0), (1,1)]) noLogAssertion , "fires propertiesModifiedEvents when assignments change" ~: test (rootCursor $ root 2 2 [AE $ coord1 (0,0), AB $ coord1 (0,1), AW $ coord1 (1,1)] []) (do logPropertyChanges modifyAssignedStones [(0,0), (0,1), (1,0), (1,1)] $ \old -> case old of Just (Just color) -> Just $ Just $ cnot color _ -> old) () (Just $ \log -> case log of -- Expect two events, since we're swapping AB and AW. But don't -- expect them in a particular order; just assert the start and end -- states. [(initial, _), (_, final)] -> (initial, final) @=? ([AB $ coord1 (0,1), AE $ coord1 (0,0), AW $ coord1 (1,1)], [AB $ coord1 (1,1), AE $ coord1 (0,0), AW $ coord1 (0,1)]) _ -> assertFailure $ "Expected two events: " ++ show log) , "doesn't fire a propertiesModifiedEvent when assignments don't change" ~: test (rootCursor $ root 2 2 [AE $ coord1 (0,0), AB $ coord1 (0,1), AW $ coord1 (1,1)] []) (do on propertiesModifiedEvent $ \old new -> tell [(sortBy (comparing propertyName) $ filter ((`elem` ["AB", "AE", "AW"]) . propertyName) old, sortBy (comparing propertyName) $ filter ((`elem` ["AB", "AE", "AW"]) . propertyName) new)] modifyAssignedStones [(0,0), (0,1), (1,0), (1,1)] id) () (Just (@?= [])) , "creates a child if the current node has a move property with no setup property" ~: test (rootCursor $ root 2 1 [B $ Just (0,0)] []) (do modifyAssignedStones [(1,0)] $ const $ Just $ Just White goToRoot cursorNode <$> getCursor) (root 2 1 [B $ Just (0,0)] [node [AW $ coord1 (1,0)]]) noLogAssertion , "doesn't create a child if the current node has a move property and a setup property" ~: test (rootCursor $ root 3 1 [B $ Just (0,0), AB $ coord1 (1,0)] []) (do logEventNames modifyAssignedStones [(2,0)] $ const $ Just $ Just White cursorNode <$> getCursor) (root 3 1 [B $ Just (0,0), AB $ coord1 (1,0), AW $ coord1 (2,0)] []) (Just (@?= ["Properties modified."])) , "fires the correct events when the current node has a move property" ~: test (rootCursor $ root 2 1 [B $ Just (0,0)] []) (do logEventNames modifyAssignedStones [(1,0)] $ const $ Just $ Just White) () -- No propertiesModifiedEvent is fired: the child is added with the -- assignment property already present. (Just (@?= ["Child added.", "Navigated."])) ] where test :: (Eq a, Show a, Eq b, Monoid b, Show b) => Cursor -> GoT (Writer b) a -> a -> Maybe (b -> Assertion) -> Assertion test cursor action expected maybeLogAssertion = do let (actual, log) = runWriter $ evalGoT action cursor expected @=? actual whenMaybe maybeLogAssertion ($ log) logPropertyChanges :: GoT (Writer [([Property], [Property])]) () logPropertyChanges = on propertiesModifiedEvent $ \old new -> tell [(sortBy (comparing propertyName) $ filter ((`elem` ["AB", "AE", "AW"]) . propertyName) old, sortBy (comparing propertyName) $ filter ((`elem` ["AB", "AE", "AW"]) . propertyName) new)] logEventNames :: GoT (Writer [String]) () logEventNames = do on0 childAddedEvent $ tell ["Child added."] on0 navigationEvent $ tell ["Navigated."] on0 propertiesModifiedEvent $ tell ["Properties modified."] noLogAssertion :: Maybe (() -> Assertion) noLogAssertion = Nothing getMarkTests = "getMark" ~: TestList [ "returns Nothing for no mark" ~: do Nothing @=? evalGo (getMark (0,0)) (rootCursor $ node []) Nothing @=? evalGo (getMark (0,0)) (rootCursor $ node [W $ Just (0,0)]) , "returns Just when a mark is present" ~: do Just MarkSquare @=? evalGo (getMark (1,2)) (rootCursor $ node [SQ $ coord1 (1,2)]) Just MarkTriangle @=? evalGo (getMark (1,1)) (rootCursor $ node [B $ Just (1,1), TR $ coord1 (1,1), SQ $ coord1 (1,2)]) , "matches all marks" ~: forM_ [minBound..maxBound] $ \mark -> Just mark @=? evalGo (getMark (0,0)) (rootCursor $ node [propertyBuilder (markProperty mark) $ coord1 (0,0)]) ] modifyMarkTests = "modifyMark" ~: TestList [ "creates a mark where there was none" ~: let cursor = rootCursor $ node [] action = do modifyMark (\Nothing -> Just MarkX) (0,0) getMark (0,0) in Just MarkX @=? evalGo action cursor , "removes an existing mark" ~: let cursor = rootCursor $ node [CR $ coord1 (0,0)] action = do modifyMark (\(Just MarkCircle) -> Nothing) (0,0) getMark (0,0) in Nothing @=? evalGo action cursor , "replaces an existing mark" ~: let cursor = rootCursor $ node [CR $ coord1 (0,0)] action = do modifyMark (\(Just MarkCircle) -> Just MarkX) (0,0) getMark (0,0) in Just MarkX @=? evalGo action cursor , "adds on to an existing mark property" ~: let cursor = rootCursor $ node [MA $ coord1 (0,0)] action = do modifyMark (\Nothing -> Just MarkX) (1,0) mapM getMark [(0,0), (1,0)] in [Just MarkX, Just MarkX] @=? evalGo action cursor , "removes from an existing mark property" ~: let cursor = rootCursor $ node [MA $ coords [(0,0), (1,0), (0,1)]] action = do modifyMark (\(Just MarkX) -> Nothing) (1,0) modifyMark (\(Just MarkX) -> Nothing) (0,1) mapM getMark [(0,0), (1,0), (0,1)] in [Just MarkX, Nothing, Nothing] @=? evalGo action cursor , "removes and adds at the same time" ~: let cursor = rootCursor $ node [MA $ coords [(0,0), (1,0), (0,1)]] action = do modifyMark (\(Just MarkX) -> Just MarkSquare) (0,0) modifyMark (\(Just MarkX) -> Nothing) (0,1) mapM getMark [(0,0), (1,0), (0,1)] in [Just MarkSquare, Just MarkX, Nothing] @=? evalGo action cursor ] addChildTests = "addChild" ~: TestList [ "adds a first child" ~: do cursorNode (execGo (addChild emptyNode) $ rootCursor emptyNode) @?= node1 [] emptyNode cursorNode (execGo (addChild $ node1 [W $ Just (1,1)] $ node [B $ Just (2,2)]) $ rootCursor $ node [B $ Just (0,0)]) @?= node1 [B $ Just (0,0)] (node1 [W $ Just (1,1)] $ node [B $ Just (2,2)]) , "adds a child to the end of the parent's child list" ~: cursorNode (execGo (addChild $ node [MN 4]) $ rootCursor $ node' [B Nothing] [node [MN 2], node [MN 3]]) @?= node' [B Nothing] [node [MN 2], node [MN 3], node [MN 4]] , "fires childAddedEvent after adding a child" ~: let cursor = rootCursor $ node' [B Nothing] [node [MN 2], node [MN 3]] action = do on childAddedEvent $ \index -> do cursor <- getCursor tell [(index, cursorNode cursor)] addChild $ node [MN 4] in execWriter (runGoT action cursor) @?= [(2, node' [B Nothing] [node [MN 2], node [MN 3], node [MN 4]])] ] addChildAtTests = "addChildAt" ~: TestList [ "adds an only child" ~: cursorNode (execGo (addChildAt 0 $ node [B Nothing]) (rootCursor $ node [])) @?= node' [] [node [B Nothing]] , "adds a first child" ~: cursorNode (execGo (addChildAt 0 $ node [B $ Just (0,0)]) (rootCursor $ node' [] [node [B $ Just (1,1)]])) @?= node' [] [node [B $ Just (0,0)], node [B $ Just (1,1)]] , "adds a middle child" ~: cursorNode (execGo (addChildAt 1 $ node [B $ Just (1,1)]) (rootCursor $ node' [] [node [B $ Just (0,0)], node [B $ Just (2,2)]])) @?= node' [] [node [B $ Just (0,0)], node [B $ Just (1,1)], node [B $ Just (2,2)]] , "adds a last child" ~: cursorNode (execGo (addChildAt 2 $ node [B $ Just (2,2)]) (rootCursor $ node' [] [node [B $ Just (0,0)], node [B $ Just (1,1)]])) @?= node' [] [node [B $ Just (0,0)], node [B $ Just (1,1)], node [B $ Just (2,2)]] , "fires childAddedEvent after adding a child" ~: let cursor = rootCursor $ node' [B Nothing] [node [MN 2], node [MN 4]] action = do on childAddedEvent $ \index -> do cursor <- getCursor tell [(index, cursorNode cursor)] addChildAt 1 $ node [MN 3] in execWriter (runGoT action cursor) @?= [(1, node' [B Nothing] [node [MN 2], node [MN 3], node [MN 4]])] , "path stack correctness" ~: TestList [ "basic case just not needing updating" ~: let cursor = child 0 $ rootCursor $ node' [B Nothing] [node [W Nothing]] action = do pushPosition True <- goUp addChildAt 1 $ node [W $ Just (0,0)] popPosition in cursorNode (execGo action cursor) @?= node [W Nothing] , "basic case just needing updating" ~: let cursor = child 0 $ rootCursor $ node' [B Nothing] [node [W Nothing]] action = do pushPosition True <- goUp addChildAt 0 $ node [W $ Just (0,0)] popPosition in cursorNode (execGo action cursor) @?= node [W Nothing] , "basic case definitely needing updating" ~: let cursor = rootCursor $ node' [B Nothing] [node [W $ Just (0,0)], node [W $ Just (1,1)]] action = do True <- goDown 1 pushPosition True <- goUp addChildAt 0 $ node [W Nothing] popPosition in cursorNode (execGo action cursor) @?= node [W $ Just (1,1)] , "multiple paths to update" ~: let at y x = B $ Just (y,x) level0Node = node' [at 0 0] $ map level1Node [0..1] level1Node i = node' [at 1 i] $ map level2Node [0..2] level2Node i = node' [at 2 i] $ map level3Node [0..3] level3Node i = node [at 3 i] action = do True <- and <$> sequence [goDown 1, goDown 2, goDown 3] pushPosition replicateM_ 3 goUp pushPosition True <- and <$> sequence [goDown 1, goDown 2, goDown 2, goUp, goDown 1] addChildAt 0 $ node1 [] $ node [] True <- and <$> sequence [goDown 0, goDown 0] goToRoot popPosition popPosition in cursorNode (execGo action $ rootCursor level0Node) @?= node [B $ Just (3,3)] , "updates paths with GoUp correctly" ~: let cursor = rootCursor $ node1 [B $ Just (0,0)] $ node [W $ Just (1,1)] action = do pushPosition True <- goDown 0 True <- goUp addChildAt 0 $ node [B $ Just (2,2)] on navigationEvent $ \step -> tell [step] popPosition log = execWriter (runGoT action cursor) in log @?= [GoDown 1, GoUp 1] ] ] deleteChildAtTests = "deleteChildAt" ~: TestList [ "ignores invalid indices" ~: do second cursorNode (runGo (deleteChildAt 0) $ rootCursor $ node []) @?= (NodeDeleteBadIndex, node []) let base = node' [] [node [MN 0]] second cursorNode (runGo (deleteChildAt (-2)) $ rootCursor base) @?= (NodeDeleteBadIndex, base) second cursorNode (runGo (deleteChildAt (-1)) $ rootCursor base) @?= (NodeDeleteBadIndex, base) second cursorNode (runGo (deleteChildAt 1) $ rootCursor base) @?= (NodeDeleteBadIndex, base) second cursorNode (runGo (deleteChildAt 2) $ rootCursor base) @?= (NodeDeleteBadIndex, base) , "deletes an only child" ~: let cursor = rootCursor $ node1 [MN 0] $ node [MN 1] action = deleteChildAt 0 in second cursorNode (runGo action cursor) @?= (NodeDeleteOk, node [MN 0]) , "deletes a first child" ~: let cursor = rootCursor $ node' [MN 0] [node [MN 1], node [MN 2]] action = deleteChildAt 0 in second cursorNode (runGo action cursor) @?= (NodeDeleteOk, node1 [MN 0] $ node [MN 2]) , "deletes middle children" ~: do let base = node' [MN 0] [node [MN 1], node [MN 2], node [MN 3], node [MN 4]] cursor = rootCursor base second cursorNode (runGo (deleteChildAt 1) cursor) @?= (NodeDeleteOk, base { nodeChildren = listDeleteAt 1 $ nodeChildren base }) second cursorNode (runGo (deleteChildAt 2) cursor) @?= (NodeDeleteOk, base { nodeChildren = listDeleteAt 2 $ nodeChildren base }) , "deletes a final child" ~: do let base = node' [MN 0] [node [MN 1], node [MN 2], node [MN 3], node [MN 4]] cursor = rootCursor base second cursorNode (runGo (deleteChildAt 1) cursor) @?= (NodeDeleteOk, base { nodeChildren = listDeleteAt 1 $ nodeChildren base }) second cursorNode (runGo (deleteChildAt 2) cursor) @?= (NodeDeleteOk, base { nodeChildren = listDeleteAt 2 $ nodeChildren base }) , "fires childDeletedEvent after deleting a child" ~: let cursor = rootCursor $ node' [MN 0] [node [MN 1], node [MN 2]] action = do on childDeletedEvent $ tell . (:[]) . (cursorChildIndex &&& cursorNode) deleteChildAt 1 in execWriter (runGoT action cursor) @?= [(1, cursorNode $ child 1 cursor)] , "path stack correctness" ~: TestList [ "basic case just not needing updating" ~: let cursor = rootCursor $ node' [B Nothing] [node [W Nothing], node [W $ Just (0,0)]] action = do True <- goDown 0 pushPosition True <- goUp NodeDeleteOk <- deleteChildAt 1 popPosition in cursorNode (execGo action cursor) @?= node [W Nothing] , "basic case just needing updating" ~: let cursor = rootCursor $ node' [B Nothing] [node [W Nothing], node [W $ Just (0,0)]] action = do True <- goDown 1 pushPosition True <- goUp NodeDeleteOk <- deleteChildAt 0 popPosition in cursorNode (execGo action cursor) @?= node [W $ Just (0,0)] , "basic case definitely needing updating" ~: let cursor = rootCursor $ node' [B Nothing] [node [W $ Just (0,0)], node [W $ Just (1,1)], node [W $ Just (2,2)]] action = do True <- goDown 2 pushPosition True <- goUp NodeDeleteOk <- deleteChildAt 0 popPosition in cursorNode (execGo action cursor) @?= node [W $ Just (2,2)] , "multiple paths to update" ~: let at y x = B $ Just (y,x) level0Node = node' [at 0 0] $ map level1Node [0..1] level1Node i = node' [at 1 i] $ map level2Node [0..2] level2Node i = node' [at 2 i] $ map level3Node [0..3] level3Node i = node [at 3 i] action = do True <- and <$> sequence [goDown 1, goDown 2, goDown 3] pushPosition True <- goUp NodeDeleteOk <- deleteChildAt 1 pushPosition goToRoot True <- goDown 0 True <- goDown 2 pushPosition True <- goUp NodeDeleteOk <- deleteChildAt 0 goToRoot True <- goDown 1 True <- goDown 2 NodeDeleteOk <- deleteChildAt 1 replicateM_ 3 popPosition in cursorNode (execGo action $ rootCursor level0Node) @?= node [B $ Just (3,3)] , "returns an error if a node to delete is on the path stack" ~: let base = node' [B $ Just (0,0)] [node [W $ Just (1,1)], node [W $ Just (2,2)]] action = do True <- goDown 1 pushPosition True <- goUp pushPosition True <- goDown 0 pushPosition True <- goUp deleteChildAt 1 in second cursorNode (runGo action $ rootCursor base) @?= (NodeDeleteOnPathStack, base) ] ] gameInfoChangedTests = "gameInfoChangedEvent" ~: TestList [ "fires when navigating down" ~: let cursor = rootCursor $ node1 [B $ Just (0,0)] $ node [W $ Just (0,0), GN $ toSimpleText "Foo"] action = do on gameInfoChangedEvent onInfo True <- goDown 0 return () in execWriter (runGoT action cursor) @?= [(Nothing, Just $ toSimpleText "Foo")] , "fires when navigating up" ~: let cursor = child 0 $ rootCursor $ node1 [B $ Just (0,0)] $ node [W $ Just (0,0), GN $ toSimpleText "Foo"] action = do on gameInfoChangedEvent onInfo True <- goUp return () in execWriter (runGoT action cursor) @?= [(Just $ toSimpleText "Foo", Nothing)] , "fires from within popPosition" ~: let cursor = rootCursor $ node1 [B $ Just (0,0)] $ node [W $ Just (0,0), GN $ toSimpleText "Foo"] action = do pushPosition True <- goDown 0 True <- goUp on gameInfoChangedEvent onInfo popPosition in execWriter (runGoT action cursor) @?= [(Nothing, Just $ toSimpleText "Foo"), (Just $ toSimpleText "Foo", Nothing)] , "fires when modifying properties" ~: let cursor = rootCursor $ node [] action = do on gameInfoChangedEvent onInfo modifyProperties $ const [GN $ toSimpleText "Foo"] modifyProperties $ const [GN $ toSimpleText "Bar"] modifyProperties $ const [] in execWriter (runGoT action cursor) @?= [(Nothing, Just $ toSimpleText "Foo"), (Just $ toSimpleText "Foo", Just $ toSimpleText "Bar"), (Just $ toSimpleText "Bar", Nothing)] ] where onInfo old new = tell [(gameInfoGameName old, gameInfoGameName new)]