packages feed

wild-bind-x11-0.2.0.0: test/WildBind/X11Spec.hs

{-# LANGUAGE FlexibleContexts, CPP, OverloadedStrings #-}
module WildBind.X11Spec (main, spec) where

import Control.Applicative ((<$>))
import Control.Concurrent.Async (async, waitCatch)
import Control.Exception (finally, Exception(fromException), throwIO)
import Control.Monad (forM_)
import Control.Monad.IO.Class (liftIO)
import qualified Control.Monad.Trans.State as State
import Data.Monoid ((<>))
import Data.List (intercalate, reverse)
import Data.IORef (IORef, newIORef, readIORef, modifyIORef)
import Data.Text (unpack)
import Data.Time.Clock (getCurrentTime, diffUTCTime)
import qualified Graphics.X11.Xlib as Xlib
import System.IO (hPutStrLn,stderr)
import Test.Hspec

import WildBind
  ( FrontEnd(frontSetGrab, frontUnsetGrab, frontNextEvent),
    FrontEvent(FEChange,FEInput), Describable, ActionDescription,
    Option(..), defOption, wildBind',
    binds', startFrom, on, as, run, whenBack, revise, justBefore
  )
import qualified WildBind.Description as WBD
import qualified WildBind.Input.NumPad as NumPad
import WildBind.X11
  ( withFrontEnd, ActiveWindow, XKeyInput,
    XMod(..), XKeyEvent(..), KeyEventType(..),
    ctrl, alt, super, shift, release, press
  )
import qualified WildBind.X11.KeySym as WKS

import WildBind.X11.TestUtil (checkIfX11Available, withGrabs)

newtype MyException = MyException String
                    deriving (Show,Eq,Ord)

instance Exception MyException

main :: IO ()
main = hspec spec

maybeRun :: Expectation -> Expectation
#ifdef TEST_INTERACTIVE
maybeRun = id
#else
maybeRun _ = pendingWith ("You need to set test-interactive flag to run the test.")
#endif

p :: String -> IO ()
p = hPutStrLn stderr . ("--- " ++)

withFrontEndForTest :: (XKeyInput i, Describable i, Ord i) => (FrontEnd ActiveWindow i -> IO a) -> IO a
withFrontEndForTest action = withFrontEnd $ \front -> do
  _ <- frontNextEvent front -- discard the first FEChange event.
  action front

grabExp :: (Bounded i, Enum i, Show i, Eq i)
        => FrontEnd ActiveWindow i -> i -> Expectation
grabExp front grab_input = grabExpMain `finally` releaseAll where
  grabExpMain = do
    frontSetGrab front grab_input
    p ("Press some numpad keys (grab="++ show grab_input ++")..")
    ev <- frontNextEvent front
    p ("Got event: " ++ show ev)
    ev `shouldBe` FEInput grab_input
  releaseAll = mapM_ (frontUnsetGrab front) (enumFromTo minBound maxBound)

grabCase :: (Bounded i, Enum i, Show i, Eq i) => FrontEnd ActiveWindow i -> Expectation
grabCase front = mapM_ (grabExp front) (enumFromTo minBound maxBound)

stopWatchMsec :: IO a -> IO (a, Int)
stopWatchMsec act = do
  start <- getCurrentTime
  ret <- act
  end <- getCurrentTime
  return (ret, floor ((diffUTCTime end start) * 1000))

describeStr :: Describable a => a -> String
describeStr = unpack . WBD.describe

unshiftNewBinding :: Eq i => IORef [[(i,ActionDescription)]] -> [(i,ActionDescription)] -> IO ()
unshiftNewBinding ref got = do
  cur <- readIORef ref
  case cur of
   [] -> update
   (latest : _) -> if latest /= got
                   then update
                   else return ()
  where
    update = modifyIORef ref (got :)

spec :: Spec
spec = checkIfX11Available $ do
  describe "X11Front" $ do
    it "should first emit FEChange event when initialized" $ withFrontEnd $ \f -> do
      p "try to get the first event..."
      (ev, time) <- stopWatchMsec $ frontNextEvent f :: IO (FrontEvent ActiveWindow NumPad.NumPadUnlocked, Int)
      time `shouldSatisfy` (< 500)
      case ev of
        FEChange _ -> return ()
        _ -> expectationFailure ("FEChange is expected, but got " ++ show ev)
    it "should NOT throw exception when it tries to double-grab in the same process" $ withFrontEnd $ \f1 ->
      withFrontEnd $ \f2 -> do
        frontSetGrab f1 NumPad.NumLeft `shouldReturn` ()
        frontSetGrab f2 NumPad.NumLeft `shouldReturn` ()
    it "should control key grab based on ('release' || 'press')" $ maybeRun $ withFrontEnd $ \f -> do
      got_binds_rev <- newIORef []
      let opt = defOption { optBindingHook = unshiftNewBinding got_binds_rev,
                            optCatch = (\_ _ e -> throwIO e)
                          }
          b_base = binds' $ do
            on (press $ ctrl $ WKS.xK_g) `as` "P(C-g)" `run` liftIO (throwIO $ MyException "NG")
            on (release $ alt $ ctrl $ WKS.xK_x) `as` "R(M-C-x)" `run` liftIO (throwIO $ MyException "OK")
          b_0 = whenBack (== 0) $ binds' $ do
            on (press $ ctrl $ WKS.xK_i) `as` "P(C-i)" `run` State.put 1
          b_1 = whenBack (== 1) $ binds' $ do
            on (press $ ctrl $ WKS.xK_i) `as` "P(C-i)" `run` State.put 0
            on (press $ alt $ ctrl $ WKS.xK_x) `as` "P(M-C-x)" `run` return ()
          rev _ _ i = justBefore $ p ("Input: " ++ (unpack $ WBD.describe i))
          b = revise rev $ startFrom (0 :: Int) $ b_base <> b_0 <> b_1
          exp_descs = [ ["P(C-g)", "R(M-C-x)", "P(C-i)"],
                        ["P(C-g)", "R(M-C-x)", "P(C-i)", "P(M-C-x)"],
                        ["P(C-g)", "R(M-C-x)", "P(C-i)"]
                      ]
      p "Input C-i C-i M-C-x. If wildBind is still blocked, then type C-g."
      result <- waitCatch =<< async (wildBind' opt b f)
      got_descs <- fmap ((map . map) snd . reverse) $ readIORef got_binds_rev
      length got_descs `shouldBe` length exp_descs
      forM_ (zip got_descs exp_descs) $ uncurry shouldMatchList
      case result of
       Right _ -> expectationFailure "expects an exception, but nothing happened."
       Left e -> fromException e `shouldBe` (Just $ MyException "OK")
      
  describe "X11Front - NumPadUnlocked" $ do
    it "should grab/ungrab keys" $ maybeRun $ withFrontEndForTest $ \f -> do
      let grabCase' :: FrontEnd ActiveWindow NumPad.NumPadUnlocked -> Expectation
          grabCase' = grabCase
      grabCase' f
  describe "X11Front - NumPadLocked" $ do
    it "should grab/ungrab keys" $ maybeRun $ withFrontEndForTest $ \f -> do
      let grabCase' :: FrontEnd ActiveWindow NumPad.NumPadLocked -> Expectation
          grabCase' = grabCase
      grabCase' f
  describe "X11Front - normal modified keys (pressed)" $ do
    it "should distinguish modifiers" $ maybeRun $ withFrontEndForTest $ \f -> do
      let inputs = [ ctrl Xlib.xK_i,
                     ctrl $ alt Xlib.xK_i,
                     super Xlib.xK_i,
                     shift $ super Xlib.xK_I
                   ]
      withGrabs f inputs $ do
        p ("Grabbed " ++ (intercalate ", " $ map describeStr inputs))
        forM_ inputs $ \input -> do
          p ("Push " ++ describeStr input)
          press_ev <- frontNextEvent f
          p ("Got event: " ++ show press_ev)
          press_ev `shouldBe` FEInput input
          release_ev <- frontNextEvent f
          p ("Got event: " ++ show release_ev)
          release_ev `shouldBe` (FEInput $ input { xKeyEventType = KeyRelease })
  describe "X11Front - Either" $ do
    it "should combine input types" $ maybeRun $ withFrontEndForTest $ \f -> do
      let inputs = [Left NumPad.NumLPeriod, Right NumPad.NumDelete]
      withGrabs f inputs $ do
        forM_ inputs $ \input -> do
          p ("Push " ++ show input)
          ev <- frontNextEvent f
          ev `shouldBe` FEInput input