packages feed

tinytools-0.1.0.4: test/Potato/Flow/GoatTester.hs

{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE RecordWildCards            #-}


module Potato.Flow.GoatTester where

import           Relude                   hiding (empty, first, fromList)

import           Test.Hspec

import           Potato.Flow
import           Potato.Flow.DebugHelpers

import           Data.Default
import qualified Data.IntMap              as IM
import qualified Data.List                as L
import           Data.Maybe               (fromJust)
import qualified Data.Sequence            as Seq
import qualified Data.Text                as T
import           Data.Tuple.Extra


type GoatTesterTrackingState = (Text, Int, Int) -- (marker, operations since start of test, operations since last marker)

-- TODO include ref to rendered canvas and then you can render it on failure cases!!
data GoatTesterRecord = GoatTesterRecord {
  _goatTesterRecord_trackingState    :: GoatTesterTrackingState
  , _goatTesterRecord_failureMessage :: Maybe Text
  , _goatTesterRecord_description    :: Text
} deriving (Show)

data GoatTesterState = GoatTesterState {
    _goatTesterState_goatState                          :: GoatState
    , _goatTesterState_rawOperationCount                :: Int

    -- TODO consider making marker more complex
    , _goatTesterState_marker                           :: Text

    , _goatTesterState_rawOperationCountSinceLastMarker :: Int
    , _goatTesterState_records                          :: [GoatTesterRecord]
  } deriving (Show)

instance Default GoatTesterState where
  def = GoatTesterState {
      _goatTesterState_goatState = makeGoatState (V2 100 100) (emptyOwlPFState, emptyControllerMeta)
      , _goatTesterState_rawOperationCount = 0
      , _goatTesterState_marker = "__START__"
      , _goatTesterState_rawOperationCountSinceLastMarker = 0
      , _goatTesterState_records = []
    }

newtype GoatTesterT m a = GoatTesterT { unGoatTesterT :: StateT GoatTesterState m a } deriving (Functor, Applicative, Monad, MonadState GoatTesterState)
type GoatTester a = GoatTesterT Identity a


runEndo :: (Monad m) => (GoatState -> GoatState) -> GoatTesterT m ()
runEndo fn = GoatTesterT $ modify $
  \gts@GoatTesterState {..} -> gts {
      _goatTesterState_goatState = fn _goatTesterState_goatState
      , _goatTesterState_rawOperationCount = _goatTesterState_rawOperationCount + 1
      , _goatTesterState_rawOperationCountSinceLastMarker = _goatTesterState_rawOperationCountSinceLastMarker + 1
    }


getGoatState :: (Monad m) => GoatTesterT m GoatState
getGoatState = GoatTesterT $ do
  GoatTesterState {..} <- get
  return _goatTesterState_goatState

getOwlPFState :: (Monad m) => GoatTesterT m OwlPFState
getOwlPFState = GoatTesterT $ do
  GoatTesterState {..} <- get
  return $ goatState_pFState _goatTesterState_goatState

getTrackingState :: (Monad m) => GoatTesterT m GoatTesterTrackingState
getTrackingState = GoatTesterT $ do
  GoatTesterState {..} <- get
  return $ (_goatTesterState_marker, _goatTesterState_rawOperationCountSinceLastMarker, _goatTesterState_rawOperationCount)

putRecord :: (Monad m) => Text -> Maybe Text -> GoatTesterT m ()
putRecord desc mf = GoatTesterT $ do
  gts <- get
  ts <- unGoatTesterT getTrackingState
  let record = GoatTesterRecord {
      _goatTesterRecord_trackingState = ts
      , _goatTesterRecord_failureMessage = mf
      , _goatTesterRecord_description = desc
    }
  put $ gts { _goatTesterState_records = record : _goatTesterState_records gts }

setMarker :: (Monad m) => Text -> GoatTesterT m ()
setMarker marker = GoatTesterT $ do
  modify $ \gts -> gts {
      _goatTesterState_marker = marker
      , _goatTesterState_rawOperationCountSinceLastMarker = 0
    }

verify :: (Monad m) => Text -> Maybe Text -> GoatTesterT m ()
verify desc mf = GoatTesterT $ do
  unGoatTesterT $ putRecord desc mf

verifyState' :: (Monad m) => Text -> (GoatState -> Maybe Text) -> GoatTesterT m Bool
verifyState' desc fn = GoatTesterT $ do
  gts <- get
  let mf = fn (_goatTesterState_goatState gts)
  unGoatTesterT $ putRecord desc mf
  return $ isJust mf

verifyState :: (Monad m) => Text -> (GoatState -> Maybe Text) -> GoatTesterT m ()
verifyState desc f = verifyState' desc f >> return ()

--verifyStateFatal :: (GoatState -> Maybe Text) -> GoatTesterT m ()
--verifyStateFatal = undefined

runGoatTesterT :: (Monad m) => GoatState -> GoatTesterT m a -> m [GoatTesterRecord]
runGoatTesterT gs m = do
  gts <- execStateT (unGoatTesterT m) $ def { _goatTesterState_goatState = gs }
  return $ reverse $ _goatTesterState_records gts

runGoatTester :: GoatState -> GoatTester a -> [GoatTesterRecord]
runGoatTester gs m = runIdentity $ runGoatTesterT gs m

hSpecGoatTesterWithOwlPFState_verbose :: OwlPFState -> GoatTester a -> SpecWith ()
hSpecGoatTesterWithOwlPFState_verbose pfs m = do
  let
    rslt' = runGoatTester (makeGoatState (V2 100 100) (pfs, emptyControllerMeta)) m
    rslt = L.groupBy (\a b -> fst3 (_goatTesterRecord_trackingState a) == fst3 (_goatTesterRecord_trackingState b)) rslt'
  forM_ rslt $ \gtss -> case gtss of
    [] -> return ()
    (x:xs) -> do
      describe (T.unpack (fst3 (_goatTesterRecord_trackingState x))) $ forM_ (x:xs) $ \GoatTesterRecord {..} -> do
        it (T.unpack _goatTesterRecord_description) $ case _goatTesterRecord_failureMessage of
          Nothing -> return ()
          Just x' -> expectationFailure (T.unpack x')


hSpecGoatTesterWithOwlPFState_quiet :: OwlPFState -> GoatTester a -> SpecWith ()
hSpecGoatTesterWithOwlPFState_quiet pfs m = do
  let
    rslt' = runGoatTester (makeGoatState (V2 100 100) (pfs, emptyControllerMeta)) m
    rslt = L.groupBy (\a b -> fst3 (_goatTesterRecord_trackingState a) == fst3 (_goatTesterRecord_trackingState b)) rslt'
  -- TODO better test case name  (pass it in)
  -- TODO accumulate and output more information on failure as single failure
  it "passes" $ forM_ rslt $ \gtss -> case gtss of
    [] -> return ()
    (x:xs) -> do
      forM_ (x:xs) $ \GoatTesterRecord {..} -> do
        case _goatTesterRecord_failureMessage of
          Nothing -> return ()
          Just x' -> expectationFailure (T.unpack x')

hSpecGoatTesterWithOwlPFState :: OwlPFState -> GoatTester a -> SpecWith ()
hSpecGoatTesterWithOwlPFState = hSpecGoatTesterWithOwlPFState_verbose

-- state getter helpers
getOwlCount :: (Monad m) => GoatTesterT m Int
getOwlCount = do
  pfs <- getOwlPFState
  return . IM.size . _owlTree_mapping . hasOwlTree_owlTree $ pfs

maybeGetMostRecentlyCreatedOwl' :: OwlPFState -> Maybe SuperOwl
maybeGetMostRecentlyCreatedOwl' pfs = maximumBy' (\s1 s2 -> compare (_superOwl_id s1) (_superOwl_id s2)) $ owliterateall (hasOwlTree_owlTree pfs)

mustGetMostRecentlyCreatedOwl :: (Monad m) => GoatTesterT m SuperOwl
mustGetMostRecentlyCreatedOwl = do
  pfs <- getOwlPFState
  return $ fromJust $ maybeGetMostRecentlyCreatedOwl' pfs


-- operation helpers

setTool :: (Monad m) => Tool -> GoatTesterT m ()
setTool tool = runEndo (endoGoatCmdSetTool tool)

setFocusArea :: (Monad m) => GoatFocusedArea -> GoatTesterT m ()
setFocusArea fa = runEndo $ endoGoatCmdSetFocusedArea fa


setDefaultParams :: (Monad m) => SetPotatoDefaultParameters -> GoatTesterT m ()
setDefaultParams params = runEndo $ endoGoatCmdSetDefaultParams params


addFolder :: (Monad m) => Text -> GoatTesterT m ()
addFolder name = runEndo $ endoGoatCmdNewFolder name

canvasMouseToScreenMouse :: GoatState -> XY -> XY
canvasMouseToScreenMouse gs pos = owlPFState_fromCanvasCoordinates (goatState_pFState gs) pos + _goatState_pan gs

mouse :: (Monad m) => Bool -> Bool -> [KeyModifier] -> MouseButton -> XY -> GoatTesterT m ()
mouse isCanvas isRelease modifiers button pos = do
  gts <- get
  runEndo $ endoGoatCmdMouse $ LMouseData {
    _lMouseData_position       = if isCanvas then canvasMouseToScreenMouse (_goatTesterState_goatState gts ) pos else pos
    , _lMouseData_isRelease    = isRelease
    , _lMouseData_button       = button
    , _lMouseData_modifiers    = modifiers
    , _lMouseData_isLayerMouse = not isCanvas
  }

canvasMouseDown :: (Monad m) =>  (Int, Int) -> GoatTesterT m ()
canvasMouseDown (x,y) = mouse True False [] MouseButton_Left (V2 x y)

canvasMouseUp :: (Monad m) =>  (Int, Int) -> GoatTesterT m ()
canvasMouseUp (x,y) = mouse True True [] MouseButton_Left (V2 x y)

canvasMouseDownUp :: (Monad m) =>  (Int, Int) -> GoatTesterT m ()
canvasMouseDownUp (x,y) = do
  canvasMouseDown (x,y)
  canvasMouseUp (x,y)

layerMouseDown :: (Monad m) =>  (Int, Int) -> GoatTesterT m ()
layerMouseDown (x,y) = mouse False False [] MouseButton_Left (V2 x y)

layerMouseUp :: (Monad m) =>  (Int, Int) -> GoatTesterT m ()
layerMouseUp (x,y) = mouse False True [] MouseButton_Left (V2 x y)

layerMouseDownUp :: (Monad m) =>  (Int, Int) -> GoatTesterT m ()
layerMouseDownUp (x,y) = do
  layerMouseDown (x,y)
  layerMouseUp (x,y)

data LayerMouseOp = LMO_Collapse | LMO_Hide | LMO_Lock | LMO_Normal deriving (Show, Eq)

layerMouseRel' :: (Monad m) => Bool -> LayerMouseOp -> Int -> Int -> GoatTesterT m ()
layerMouseRel' isup op y' depth = do
  scrollPos <- (_layersState_scrollPos . _goatState_layersState . _goatTesterState_goatState) <$> get
  let
    x' = case op of
      LMO_Collapse -> 0
      LMO_Hide     -> 1
      LMO_Lock     -> 2
      LMO_Normal -> 3
    y = y' - scrollPos
    x = x' + depth
  putRecord "validate y pos" (if y < 0 then Just ("y: " <> show y <> " outside of scroll position: " <> show scrollPos) else Nothing)
  if isup
    then layerMouseUp (x, y)
    else layerMouseDown (x, y)

layerMouseDownRel :: (Monad m) => LayerMouseOp -> Int -> Int -> GoatTesterT m ()
layerMouseDownRel = layerMouseRel' False

layerMouseUpRel :: (Monad m) => LayerMouseOp -> Int -> Int -> GoatTesterT m ()
layerMouseUpRel = layerMouseRel' True

layerMouseDownUpRel :: (Monad m) => LayerMouseOp -> Int -> Int -> GoatTesterT m ()
layerMouseDownUpRel op x y = do
  layerMouseDownRel op x y
  layerMouseUpRel op x y


pressKeyboardKey :: (Monad m) => KeyboardKey -> GoatTesterT m ()
pressKeyboardKey k = runEndo $ endoGoatCmdKeyboard (KeyboardData k [])

pressKey :: (Monad m) => Char -> GoatTesterT m ()
pressKey c = pressKeyboardKey (KeyboardKey_Char c)

pressKeys :: (Monad m) => String -> GoatTesterT m ()
pressKeys text = forM_ text $ \c -> pressKeyboardKey (KeyboardKey_Char c)

pressEscape :: (Monad m) => GoatTesterT m ()
pressEscape = pressKeyboardKey KeyboardKey_Esc

pressReturn :: (Monad m) => GoatTesterT m ()
pressReturn = pressKeyboardKey KeyboardKey_Return

pressBackspace :: (Monad m) => GoatTesterT m ()
pressBackspace = pressKeyboardKey KeyboardKey_Backspace

pressDelete :: (Monad m) => GoatTesterT m ()
pressDelete = pressKeyboardKey KeyboardKey_Delete

pressUndo :: (Monad m) => GoatTesterT m ()
pressUndo = runEndo $ endoGoatCmdKeyboard  (KeyboardData (KeyboardKey_Char 'z') [KeyModifier_Ctrl])

pressRedo :: (Monad m) => GoatTesterT m ()
pressRedo = runEndo $ endoGoatCmdKeyboard  (KeyboardData (KeyboardKey_Char 'y') [KeyModifier_Ctrl])


markSaved :: (Monad m) => GoatTesterT m ()
markSaved = runEndo $ endoGoatCmdMarkSaved ()

-- verification helpers

-- | verifies that the number of owls (elts) in the state is what is expected, includes folders in the count
verifyOwlCount :: (Monad m) => Int -> GoatTesterT m ()
verifyOwlCount expected = verifyState "verifyOwlCount" f where
  f gs = if n == expected
    then Nothing
    else Just $ "got " <> show n <> " owls, expected " <> show expected
    where n = IM.size . _owlTree_mapping . hasOwlTree_owlTree . goatState_pFState $ gs

-- | verifies that the number of selected owls (elts) is what's expected (uses regular selection, NOT canvas selection)
verifySelectionCount :: (Monad m) => Int -> GoatTesterT m ()
verifySelectionCount expected = verifyState "verifySelectionCount" f where
  f gs = if n == expected
    then Nothing
    else Just $ "got " <> show n <> " selected owls, expected " <> show expected
    where n = isParliament_length . _goatState_selection $ gs

maximumBy' :: Foldable t => (a -> a -> Ordering) -> t a -> Maybe a
maximumBy' f l = if length l == 0
  then Nothing
  else Just $ L.maximumBy f l

verifyMostRecentlyCreatedOwl' :: (Monad m) => Text -> (SuperOwl -> Maybe Text) -> GoatTesterT m ()
verifyMostRecentlyCreatedOwl' desc f = verifyState desc f' where
  f' gs = r where
    msowl = maybeGetMostRecentlyCreatedOwl' (goatState_pFState gs)
    r = case msowl of
      Nothing -> Just "failed, no 🦉s"
      Just sowl -> (\m -> "failed with message: " <> m <> "\ngot:\n" <> potatoShow (_superOwl_elt sowl)) <$> f sowl

verifyMostRecentlyCreatedOwl :: (Monad m) => (SuperOwl -> Maybe Text) -> GoatTesterT m ()
verifyMostRecentlyCreatedOwl = verifyMostRecentlyCreatedOwl' "verifyMostRecentlyCreatedOwl"

verifyMostRecentlyCreatedLine :: (Monad m) => (SAutoLine -> Maybe Text) -> GoatTesterT m ()
verifyMostRecentlyCreatedLine f = verifyMostRecentlyCreatedOwl' "verifyMostRecentlyCreatedLine" f' where
  f' sowl = case _owlItem_subItem (_superOwl_elt sowl) of
    OwlSubItemLine sline -> f sline
    x                      -> Just $ "expected SAutoLine got: " <> show x

verifySelectionIsAndOnlyIs :: (Monad m) => Text -> (SuperOwl -> Maybe Text) -> GoatTesterT m ()
verifySelectionIsAndOnlyIs desc f = verifyState desc f' where
  f' gs = r where
    SuperOwlParliament selection = _goatState_selection gs
    sowl = Seq.index selection 0
    nselection = Seq.length selection
    r = if nselection /= 1
      then Just $ "failed, expected 1 selected 🦉, got " <> show nselection
      else (\m -> "failed with message: " <> m <> "\ngot:\n" <> potatoShow (_superOwl_elt sowl)) <$> f sowl

-- otheruseful stuff

-- export as part of this module becaues it's super useful
-- from https://hackage.haskell.org/package/utility-ht-0.0.16/docs/src/Data.Maybe.HT.html#toMaybe
{-# INLINE toMaybe #-}
toMaybe :: Bool -> a -> Maybe a
toMaybe False _ = Nothing
toMaybe True  x = Just x

alwaysFail :: (Monad m) => Text -> GoatTesterT m ()
alwaysFail msg = GoatTesterT $ do
  unGoatTesterT $ putRecord "this test always fails" (Just msg)