{-# LANGUAGE OverloadedStrings #-} module WildBind.ExecSpec (main, spec) where import Control.Applicative ((<$>)) import Control.Concurrent (forkIOWithUnmask, killThread, threadDelay) import Control.Concurrent.STM (atomically, TChan, readTChan, tryReadTChan, writeTChan, newTChanIO) import Control.Exception (bracket, throw, fromException) import Control.Monad.IO.Class (MonadIO, liftIO) import qualified Control.Monad.Trans.State as State import Data.Monoid ((<>)) import System.IO.Error (userError) import Test.Hspec import qualified WildBind.Binding as WBB import qualified WildBind.Exec as WBE import qualified WildBind.FrontEnd as WBF import WildBind.ForTest (SampleInput(..), SampleState(..), SampleBackState(..)) newtype EventChan s i = EventChan { unEventChan :: TChan (WBF.FrontEvent s i) } data GrabHistory i = GSet i | GUnset i deriving (Show, Eq, Ord) newtype GrabChan i = GrabChan { unGrabChan :: TChan (GrabHistory i) } frontEnd :: EventChan s i -> GrabChan i -> WBF.FrontEnd s i frontEnd echan gchan = WBF.FrontEnd { WBF.frontDefaultDescription = const "", WBF.frontSetGrab = \i -> atomically $ writeTChan (unGrabChan gchan) (GSet i), WBF.frontUnsetGrab = \i -> atomically $ writeTChan (unGrabChan gchan) (GUnset i), WBF.frontNextEvent = atomically $ readTChan $ unEventChan echan } _write :: MonadIO m => TChan a -> a -> m () _write tc = liftIO . atomically . writeTChan tc outChanOn :: MonadIO m => TChan a -> i -> a -> (i, WBB.Action m ()) outChanOn out_chan input out_elem = (input, WBB.Action "" (out_chan `_write` out_elem)) outChanOnS :: TChan a -> i -> a -> bs -> (i, WBB.Action (State.StateT bs IO) ()) outChanOnS out_chan input out_elem next_state = (,) input $ WBB.Action "" $ do liftIO $ atomically $ writeTChan out_chan out_elem State.put next_state withWildBind' :: (WBF.FrontEnd s i -> IO ()) -> (EventChan s i -> GrabChan i -> IO ()) -> IO () withWildBind' exec action = do echan <- EventChan <$> newTChanIO gchan <- GrabChan <$> newTChanIO let spawnWildBind = forkIOWithUnmask $ \umask -> umask $ exec (frontEnd echan gchan) bracket spawnWildBind killThread (\_ -> action echan gchan) withWildBind :: Ord i => WBB.Binding s i -> (EventChan s i -> GrabChan i -> IO ()) -> IO () withWildBind binding action = withWildBind' (WBE.wildBind binding) action emitEvent :: TChan (WBF.FrontEvent s i) -> WBF.FrontEvent s i -> IO () emitEvent chan event = atomically $ writeTChan chan event shouldProduce :: (Show a, Eq a) => TChan a -> a -> IO () shouldProduce chan expectation = (atomically $ readTChan chan) `shouldReturn` expectation readAll :: TChan a -> IO [a] readAll chan = atomically $ readAll' [] where readAll' acc = do mret <- tryReadTChan chan case mret of Nothing -> return (reverse acc) Just ret -> readAll' (ret : acc) shouldNowMatch :: (Show a, Eq a) => TChan a -> [a] -> IO () shouldNowMatch chan expectation = readAll chan >>= (`shouldMatchList` expectation) changeAndInput :: s -> i -> [WBF.FrontEvent s i] changeAndInput s i = [WBF.FEChange s, WBF.FEInput i] main :: IO () main = hspec spec spec :: Spec spec = do wildBindSpec optionSpec wildBindSpec :: Spec wildBindSpec = do describe "wildBind" $ do it "should enable input grabs" $ do ochan <- newTChanIO let b = WBB.binding [outChanOn ochan SIa 'A', outChanOn ochan SIb 'B'] withWildBind b $ \(EventChan echan) (GrabChan gchan) -> do emitEvent echan $ WBF.FEChange $ SS "" emitEvent echan $ WBF.FEInput SIa ochan `shouldProduce` 'A' ghist <- readAll gchan ghist `shouldMatchList` [GSet SIa, GSet SIb] it "should enable/disable grabs when the front-end state changes" $ do ochan <- newTChanIO let b = (WBB.whenFront (\(SS s) -> s == "A") $ WBB.binding [outChanOn ochan SIa 'A']) <> (WBB.whenFront (\(SS s) -> s == "B") $ WBB.binding [outChanOn ochan SIb 'B']) <> (WBB.whenFront (\(SS s) -> s == "C") $ WBB.binding [outChanOn ochan SIc 'C']) withWildBind b $ \(EventChan echan) (GrabChan gchan) -> do mapM_ (emitEvent echan) $ changeAndInput (SS "A") SIa ochan `shouldProduce` 'A' gchan `shouldNowMatch` [GSet SIa] mapM_ (emitEvent echan) $ changeAndInput (SS "B") SIb ochan `shouldProduce` 'B' gchan `shouldNowMatch` [GUnset SIa, GSet SIb] mapM_ (emitEvent echan) $ changeAndInput (SS "C") SIc ochan `shouldProduce` 'C' gchan `shouldNowMatch` [GUnset SIb, GSet SIc] emitEvent echan $ WBF.FEChange (SS "") threadDelay 10000 gchan `shouldNowMatch` [GUnset SIc] it "should enable/disable grabs when the back-end state changes" $ do ochan <- newTChanIO let b' = WBB.ifBack (== (SB 0)) (WBB.binding' [outChanOnS ochan SIa 'A' (SB 1)]) $ WBB.whenBack (== (SB 1)) (WBB.binding' [outChanOnS ochan SIb 'B' (SB 0)]) b = WBB.startFrom (SB 0) b' withWildBind b $ \(EventChan echan) (GrabChan gchan) -> do emitEvent echan $ WBF.FEChange (SS "") threadDelay 10000 gchan `shouldNowMatch` [GSet SIa] emitEvent echan $ WBF.FEInput SIa ochan `shouldProduce` 'A' threadDelay 10000 gchan `shouldNowMatch` [GUnset SIa, GSet SIb] emitEvent echan $ WBF.FEInput SIb ochan `shouldProduce` 'B' threadDelay 10000 gchan `shouldNowMatch` [GUnset SIb, GSet SIa] it "should crush exceptions from bound actions" $ do ochan <- newTChanIO let b = WBB.binds $ do WBB.on SIa `WBB.run` (fail "ERROR!!") WBB.on SIb `WBB.run` (atomically $ writeTChan ochan 'b') withWildBind b $ \(EventChan echan) _ -> do emitEvent echan $ WBF.FEChange (SS "") emitEvent echan $ WBF.FEInput SIa emitEvent echan $ WBF.FEInput SIb ochan `shouldProduce` 'b' it "should keep the current back-state when exception is thrown from bound actions" $ do ochan <- newTChanIO let killing_b = WBB.binds' $ WBB.on SIa `WBB.run` do State.put (SB 0) liftIO $ fail "ERROR!" b = WBB.startFrom (SB 0) $ (killing_b <>) $ WBB.ifBack (== SB 0) ( WBB.binds' $ WBB.on SIb `WBB.run` do liftIO $ atomically $ writeTChan ochan 'b' State.put (SB 1) ) ( WBB.binds' $ WBB.on SIc `WBB.run` do liftIO $ atomically $ writeTChan ochan 'c' State.put (SB 0) ) withWildBind b $ \(EventChan echan) (GrabChan gchan) -> do emitEvent echan $ WBF.FEChange (SS "") emitEvent echan $ WBF.FEInput SIa emitEvent echan $ WBF.FEInput SIb ochan `shouldProduce` 'b' gchan `shouldNowMatch` [GSet SIa, GSet SIb, GUnset SIb, GSet SIc] emitEvent echan $ WBF.FEInput SIa emitEvent echan $ WBF.FEInput SIc ochan `shouldProduce` 'c' gchan `shouldNowMatch` [GUnset SIc, GSet SIb] shouldNextMatch :: (Show a, Eq a) => TChan [a] -> [a] -> IO () shouldNextMatch tc expected = do got <- atomically $ readTChan tc got `shouldMatchList` expected optionSpec :: Spec optionSpec = do describe "optBindingHook" $ do it "hooks change of binding because front-end state changes" $ do hook_chan <- newTChanIO out_chan <- newTChanIO let opt = WBE.defOption { WBE.optBindingHook = _write hook_chan } b = WBB.whenFront (== SS "hoge") $ WBB.binding [ (SIa, WBB.Action "a button" (out_chan `_write` 'a')), (SIb, WBB.Action "b button" (out_chan `_write` 'b')) ] withWildBind' (WBE.wildBind' opt b) $ \(EventChan echan) (GrabChan gchan) -> do emitEvent echan $ WBF.FEChange (SS "hoge") hook_chan `shouldNextMatch` [(SIa, "a button"), (SIb, "b button")] emitEvent echan $ WBF.FEInput SIa hook_chan `shouldNextMatch` [(SIa, "a button"), (SIb, "b button")] out_chan `shouldProduce` 'a' gchan `shouldNowMatch` [GSet SIa, GSet SIb] emitEvent echan $ WBF.FEChange (SS "") hook_chan `shouldNextMatch` [] gchan `shouldNowMatch` [GUnset SIa, GUnset SIb] it "hooks change of binding because back-end state changes" $ do hook_chan <- newTChanIO out_chan <- newTChanIO let opt = WBE.defOption { WBE.optBindingHook = _write hook_chan } b = WBB.startFrom (SB 0) $ WBB.ifBack (== (SB 0)) (WBB.binding' [(SIa, WBB.Action "a button" (out_chan `_write` 'a' >> State.put (SB 1)))]) $ WBB.whenBack (== (SB 1)) ( WBB.binding' [(SIa, WBB.Action "A BUTTON" (out_chan `_write` 'A' >> State.put (SB 0))), (SIc, WBB.Action "c button" (out_chan `_write` 'c'))] ) withWildBind' (WBE.wildBind' opt b) $ \(EventChan echan) (GrabChan gchan) -> do emitEvent echan $ WBF.FEChange (SS "") hook_chan `shouldNextMatch` [(SIa, "a button")] gchan `shouldNowMatch` [GSet SIa] emitEvent echan $ WBF.FEInput SIa out_chan `shouldProduce` 'a' hook_chan `shouldNextMatch` [(SIa, "A BUTTON"), (SIc, "c button")] gchan `shouldNowMatch` [GSet SIc] emitEvent echan $ WBF.FEInput SIc out_chan `shouldProduce` 'c' hook_chan `shouldNextMatch` [(SIa, "A BUTTON"), (SIc, "c button")] gchan `shouldNowMatch` [] emitEvent echan $ WBF.FEInput SIa out_chan `shouldProduce` 'A' hook_chan `shouldNextMatch` [(SIa, "a button")] gchan `shouldNowMatch` [GUnset SIc] describe "optCatch" $ do it "receives front-state, input and exception" $ do hook_chan <- newTChanIO let catcher fs input err = atomically $ writeTChan hook_chan (fs, input, err) opt = WBE.defOption { WBE.optCatch = catcher } b = WBB.binds $ WBB.on SIa `WBB.run` (throw $ userError "BOOM!") withWildBind' (WBE.wildBind' opt b) $ \(EventChan echan) _ -> do emitEvent echan $ WBF.FEChange (SS "front state") emitEvent echan $ WBF.FEInput SIa (got_state, got_input, got_exception) <- atomically $ readTChan hook_chan got_state `shouldBe` SS "front state" got_input `shouldBe` SIa fromException got_exception `shouldBe` Just (userError "BOOM!")