packages feed

zuramaru-0.1.0.0: test/tasty/MaruTest.hs

{-# LANGUAGE OverloadedStrings #-}

-- | The functions for help tests
module MaruTest
  ( StoppedPoint (..)
  , runCodeInstantly
  , runCode
  , runCodes
  , runCodeWithSteps
  , prettyAssertFailure
  , shouldBeEvaluatedTo
  , shouldBeEvaluatedTo'
  , isExistedIn
  , isNotExistedIn
  , runCorretly
  , runCodesCorrectly
  ) where

import Control.Lens (_1, _2, view, to, (<&>))
import Control.Monad (void, (>=>))
import Data.List (foldl')
import Data.List.NonEmpty (NonEmpty(..))
import Data.Text (Text)
import Maru.Type
import System.IO.Silently (silence)
import Test.Tasty.HUnit (Assertion, assertFailure, (@?=))
import qualified Data.Text.IO as TIO
import qualified Maru.Eval as E
import qualified Maru.Parser as P
import qualified Maru.Preprocessor as Pr

-- |
-- `ParseError` gives a cause of the parse error.
-- `EvalError` is same as `ParseError` but for the evaluation.
-- `Succeed` gives the results of the execution.
-- ("execution" means "parse" + "evaluate")
--
-- Please see `runCodeWithSteps` for more information.
data StoppedPoint = ParseError String
                  | EvalError String
                  | Succeed SExpr MaruEnv SimplificationSteps
  deriving (Show)


-- |
-- Similar to `runCode`.
-- Run maru's source code in new `MaruEnv`,
runCodeInstantly :: SourceCode -> IO (SExpr, MaruEnv, SimplificationSteps)
runCodeInstantly = runCode E.initialEnv

-- |
-- Similar to `runCodeWithSteps`,
-- but `ParseError` and `EvalError` are thrown as the context of `IO`
runCode :: MaruEnv -> SourceCode -> IO (SExpr, MaruEnv, SimplificationSteps)
runCode env code = do
  result <- runCodeWithSteps env code
  case result of
    EvalError  msg -> fail msg
    ParseError msg -> fail msg
    Succeed x y z -> return (x, y, z)

-- |
-- Similar to 'runCode', but multi expressions are taken.
-- And the previous result is continued.
runCodes :: MaruEnv -> NonEmpty SourceCode -> IO (SExpr, MaruEnv, SimplificationSteps)
runCodes env (code:|codes) = do
  let runners = flip map codes $ \x -> flip runCode x . view _2
  let tailExecutor = foldl' (>=>) (flip runCode code . view _2) runners
  runCode env "()" >>= tailExecutor

-- |
-- Run maru's source code in the specified `MaruEnv`.
--
-- If the parsing is failed, return `ParseError` .
-- If the evaluation is failed, return `EvalError`.
-- If all procedure is succeed, return `Succeed`.
runCodeWithSteps :: MaruEnv -> SourceCode -> IO StoppedPoint
runCodeWithSteps env code = do
  result <- mapM (E.eval env . Pr.preprocess) $ P.parse code
  case result of
    Left e                  -> return . ParseError $ show e
    Right (Left e)          -> return . EvalError $ show e
    Right (Right (x, y, z)) -> return $ Succeed x y z

-- |
-- Pretty print the results,
-- and Make the failure this `Assertion`
prettyAssertFailure :: SExpr -> MaruEnv -> SimplificationSteps -> String -> Assertion
prettyAssertFailure sexpr env steps msg = do
  putStrLn "Fail"
  putStrLn msg
  putStrLn $ "got output result: " ++ show sexpr
  putStrLn $ "got final environment: " ++ show env
  putStrLn "got logs:"
  mapM_ TIO.putStrLn $ reportSteps steps
  assertFailure ""


-- | 'code' can be evaluated to 'expected'
shouldBeEvaluatedTo :: SourceCode -> SourceCode -> Assertion
shouldBeEvaluatedTo code expected = do
  (sexpr, _, _) <- runCodeInstantly code
  readable sexpr @?= expected

-- | Similar to 'shouldBeEvaluatedTo' (comparison) and 'runCodes' (multi codes)
shouldBeEvaluatedTo' :: NonEmpty SourceCode -> SourceCode -> Assertion
shouldBeEvaluatedTo' codes expected = do
  actual <- silence $ runCodes E.initialEnv codes <&> view (_1 . to readable)
  actual @?= expected


-- | 'var' is existed in 'env'
isExistedIn :: MaruSymbol -> MaruEnv -> Assertion
var `isExistedIn` env = void . runCode env $ unMaruSymbol var

-- | 'var' is not existed in 'env'
isNotExistedIn :: MaruSymbol -> MaruEnv -> Assertion
var `isNotExistedIn` env = do
  point <- runCodeWithSteps env $ unMaruSymbol var
  case point of
    EvalError _ -> return ()
    x           -> assertFailure $ show x


-- | Mean that it returns something without the result
runCorretly :: Text -> Assertion
runCorretly = void . runCodeInstantly

-- | Similar to 'runCorretly' and 'shouldBeEvaluatedTo'`
runCodesCorrectly :: NonEmpty Text -> Assertion
runCodesCorrectly = void . runCodes E.initialEnv