packages feed

webdriver-precore-0.2.0.0: test/IOUtils.hs

module IOUtils
  ( sleep,
    encodeFileToBase64,
    exceptionTextIncludes,
    logNothingLogger,
    lwrTxtIncludes,
    DemoActions (..),
    noOpUtils,
    mkDemoActions,
    (===),
    findWebDriverRoot,
    Logger (..),
    loopForever,
    catchLog,
    defToLogNothing
  )
where

import AesonUtils (prettyJSON)
import Const (Timeout (..), seconds)
import Control.Concurrent (threadDelay)
import Control.Exception (Exception (..), Handler (..), SomeException, catches)
import Control.Monad (void, when)
import Data.Aeson (Value)
import Data.Base64.Types qualified as B64T
import Data.ByteString qualified as BS
import Data.ByteString.Base64 qualified as B64
import Data.Maybe (fromMaybe)
import Data.Text (Text, isInfixOf, pack, toLower, unpack)
import GHC.Base (coerce)
import System.FilePath (joinPath, splitDirectories, (</>))
import Test.Tasty.HUnit as HUnit (Assertion, HasCallStack, (@=?))
import UnliftIO (AsyncCancelled, async, atomically, race_, readTMVar, throwIO, tryPutTMVar)
import UnliftIO.Async (Async)
import UnliftIO.STM (newEmptyTMVarIO)
import Utils (txt)
import Prelude hiding (log)

findWebDriverRoot :: FilePath -> Maybe FilePath
findWebDriverRoot path =
  if rootDir `elem` dirs
    then Just webDriverPath
    else Nothing
  where
    rootDir = "webdriver"
    dirs = splitDirectories path
    webDriverPath = (joinPath $ takeWhile (/= rootDir) dirs) </> rootDir

newtype Logger = MkLogger
  { log :: Text -> IO ()
  }

logNothingLogger :: Logger
logNothingLogger = MkLogger . const $ pure ()

-- default Nothing to Logger that does nothing
defToLogNothing :: Maybe Logger -> Logger
defToLogNothing = fromMaybe logNothingLogger

mkDemoActions :: Logger -> Timeout -> DemoActions
mkDemoActions qLog pause =
  MkDemoActions
    { sleep,
      log = log',
      logShow = logShow',
      logJSON,
      logM = \l mt -> mt >>= log' l,
      logShowM = \l t -> t >>= logShow' l,
      logTxt = logTxt',
      pause = sleep pause,
      pauseAtLeast = pauseAtLeast pause,
      timeLimitLog = timeLimitLog hasEventFired,
      timeLimitLogMany = timeLimitLogMany hasEventFired,
      timeLimitLog' = \msg tmo -> timeLimitLog' msg hasEventFired tmo
    }
  where
    logTxt' :: Text -> IO ()
    logTxt' = qLog.log
    log' l t = logTxt' $ l <> ": " <> t
    logJSON msg val = prettyJSON msg val >>= logTxt'
    logShow' :: forall a. (Show a) => Text -> a -> IO ()
    logShow' l = log' l . txt
    hasEventFired :: forall a b. (Show a, Show b) => b -> a -> IO Bool
    hasEventFired b a = logShow' ("!!!!! " <> txt b <> " Event Fired !!!!!\n") a >> pure True

exceptionTextIncludes :: Text -> SomeException -> Bool
exceptionTextIncludes expected = lwrTxtIncludes expected . pack . displayException

lwrTxtIncludes :: Text -> Text -> Bool
lwrTxtIncludes expected = (toLower expected `isInfixOf`) . toLower

data DemoActions = MkDemoActions
  { sleep :: Timeout -> IO (),
    pause :: IO (),
    pauseAtLeast :: Timeout -> IO (),
    logTxt :: Text -> IO (),
    log :: Text -> Text -> IO (),
    logShow :: forall a. (Show a) => Text -> a -> IO (),
    logJSON :: Text -> Value -> IO (),
    logM :: Text -> IO Text -> IO (),
    logShowM :: forall a. (Show a) => Text -> IO a -> IO (),
    -- timeout functions for testing events TODO: rename
    timeLimitLog :: forall a b. (Show a, Show b) => b -> IO (a -> IO (), IO ()),
    timeLimitLogMany :: forall a b. (Show a, Show b) => b -> IO (a -> IO (), IO ()),
    timeLimitLog' :: forall a b. (Show a, Show b) => Text -> Timeout -> b -> IO (a -> IO (), IO ())
  }

noOpUtils :: DemoActions
noOpUtils =
  MkDemoActions
    { sleep = noOp1,
      logTxt = noOp1,
      log = noOp2,
      logShow = noOp2,
      logJSON = noOp2,
      logM = noOp2,
      logShowM = noOp2,
      pause = pure (),
      -- even for no-op, ensure sleep for passed in ms
      pauseAtLeast = sleep,
      timeLimitLog = timeLimitLog hasEventFired,
      timeLimitLogMany = timeLimitLogMany hasEventFired,
      timeLimitLog' = \msg tmo -> timeLimitLog' msg hasEventFired tmo
    }
  where
    noOp1 = const $ pure ()
    noOp2 = const . const $ pure ()

    hasEventFired :: b -> a -> IO Bool
    hasEventFired _b _a = pure True

sleep :: Timeout -> IO ()
sleep = threadDelay . coerce

pauseAtLeast :: Timeout -> Timeout -> IO ()
pauseAtLeast defaultSleep = sleep . MkTimeout . max (coerce defaultSleep) . coerce

timeLimit :: forall a b. (Show b) => Text -> Timeout -> b -> (a -> IO Bool) -> IO (a -> IO (), IO ())
timeLimit timeoutMsg (MkTimeout mu) eventDesc action = do
  triggered <- newEmptyTMVarIO
  let waitTriggered = atomically $ readTMVar triggered
      waitLimit = threadDelay mu >> (fail . unpack $ "Timeout - " <> timeoutMsg <> ": " <> txt eventDesc <> " after " <> txt (mu `div` 1000) <> " milliseconds")
      interceptedAction = \a -> do
        result <- action a
        when result $
          void $
            atomically $
              tryPutTMVar triggered ()
        pure ()
  pure $ (interceptedAction, race_ waitLimit waitTriggered)

timeLimitLog' :: forall a b. (Show b) => Text -> (b -> a -> IO Bool) -> Timeout -> b -> IO (a -> IO (), IO ())
timeLimitLog' timeoutMsg prd timeout b =
  timeLimit timeoutMsg timeout b (\a -> prd b a >> pure True)

timeLimitLog :: forall a b. (Show b) => (b -> a -> IO Bool) -> b -> IO (a -> IO (), IO ())
timeLimitLog prd = timeLimitLog' "Expected event did not fire" prd (10 * seconds)

timeLimitLogMany :: forall a b. (Show b) => (b -> a -> IO Bool) -> b -> IO (a -> IO (), IO ())
timeLimitLogMany prd = timeLimitLog' "Expected event(s) did not fire (Many Subscription)" prd (10 * seconds)

encodeFileToBase64 :: FilePath -> IO Text
encodeFileToBase64 filePath =
  B64T.extractBase64 . B64.encodeBase64 <$> BS.readFile filePath

(===) ::
  (Eq a, Show a, HasCallStack) =>
  -- | The actual value
  a ->
  -- | The expected value
  a ->
  Assertion
(===) = (@=?)

catchLog :: Text -> (Text -> IO ()) -> IO () -> IO ()
catchLog name logAction action =
  action
    `catches` [ Handler $ \(e :: AsyncCancelled) -> do
                  logAction $ name <> " thread cancelled"
                  throwIO e,
                Handler $ \(e :: SomeException) -> do
                  logAction $ "Exception thrown in " <> name <> " thread" <> ": " <> (pack $ displayException e)
                  throwIO e
              ]

-- | like forever but, unlike forever, it fails if an exception is thrown
loopForever :: (Text -> IO ()) -> Text -> IO () -> IO (Async ())
loopForever logger name action = async $ do
  logger $ "Starting " <> name <> " thread"
  loop
  where
    loop = catchLog name logger action >> loop