{-# LANGUAGE CPP #-}
module TestMode (
testMode,
) where
import Control.Monad
import Control.Monad.Except
import Control.Monad.Reader
import Control.Monad.State
import Data.Bifunctor
import Data.List
import Data.Maybe
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.IO qualified as T
import System.IO.Error
import Text.Megaparsec.Error
import Text.Megaparsec.Pos
import Config
import Output
import Parser
import Run
import Script.Expr
import Script.Module
import Test
data TestModeInput = TestModeInput
{ tmiOutput :: Output
, tmiConfig :: Maybe Config
, tmiParams :: [ Text ]
}
data TestModeState = TestModeState
{ tmsModules :: [ Module ]
, tmsGlobals :: GlobalDefs
, tmsNextTestNumber :: Int
}
initTestModeState :: TestModeState
initTestModeState = TestModeState
{ tmsModules = mempty
, tmsGlobals = mempty
, tmsNextTestNumber = 1
}
testMode :: Maybe Config -> IO ()
testMode tmiConfig = do
tmiOutput <- startOutput OutputStyleTest False
let testLoop = getLineMb >>= \case
Just line -> do
case T.words line of
cname : tmiParams
| Just (CommandM cmd) <- lookup cname commands -> do
runReaderT cmd $ TestModeInput {..}
| otherwise -> fail $ "Unknown command '" ++ T.unpack cname ++ "'"
[] -> return ()
testLoop
Nothing -> return ()
runExceptT (evalStateT testLoop initTestModeState) >>= \case
Left err -> flip runReaderT tmiOutput $ outLine OutputError Nothing $ T.pack err
Right () -> return ()
getLineMb :: MonadIO m => m (Maybe Text)
getLineMb = liftIO $ catchIOError (Just <$> T.getLine) (\e -> if isEOFError e then return Nothing else ioError e)
cmdOut :: Text -> Command
cmdOut line = do
out <- asks tmiOutput
flip runReaderT out $ outLine OutputTestRaw Nothing line
getNextTestNumber :: CommandM Int
getNextTestNumber = do
num <- gets tmsNextTestNumber
modify $ \s -> s { tmsNextTestNumber = num + 1 }
return num
runSingleTest :: Test -> CommandM Bool
runSingleTest test = do
out <- asks tmiOutput
num <- getNextTestNumber
globals <- gets tmsGlobals
mbconfig <- asks tmiConfig
let opts = defaultTestOptions
{ optDefaultTool = fromMaybe "" $ configTool =<< mbconfig
, optTestDir = ".test" <> show num
, optKeep = True
}
liftIO (runTest out opts globals test)
newtype CommandM a = CommandM (ReaderT TestModeInput (StateT TestModeState (ExceptT String IO)) a)
deriving
( Functor, Applicative, Monad, MonadIO
, MonadReader TestModeInput, MonadState TestModeState, MonadError String
)
instance MonadFail CommandM where
fail = throwError
type Command = CommandM ()
commands :: [ ( Text, Command ) ]
commands =
[ ( "load", cmdLoad )
, ( "load-config", cmdLoadConfig )
, ( "run", cmdRun )
, ( "run-all", cmdRunAll )
]
cmdLoad :: Command
cmdLoad = do
[ path ] <- asks tmiParams
liftIO (parseTestFiles [ T.unpack path ]) >>= \case
Right ( modules, allModules ) -> do
let globalDefs = evalGlobalDefs $ concatMap (\m -> map (first ( moduleName m, )) $ moduleDefinitions m) allModules
modify $ \s -> s
{ tmsModules = modules
, tmsGlobals = globalDefs
}
cmdOut "load-done"
Left (ModuleNotFound moduleName) -> do
cmdOut $ "load-failed module-not-found" <> textModuleName moduleName
Left (FileNotFound notFoundPath) -> do
cmdOut $ "load-failed file-not-found " <> T.pack notFoundPath
Left (ImportModuleError bundle) -> do
#if MIN_VERSION_megaparsec(9,7,0)
mapM_ (cmdOut . T.pack) $ lines $ errorBundlePrettyWith showParseError bundle
#endif
cmdOut $ "load-failed parse-error"
where
showParseError _ SourcePos {..} _ = concat
[ "parse-error"
, " ", sourceName
, ":", show $ unPos sourceLine
, ":", show $ unPos sourceColumn
]
cmdLoadConfig :: Command
cmdLoadConfig = do
Just config <- asks tmiConfig
( modules, globalDefs ) <- liftIO $ loadModules =<< getConfigTestFiles config
modify $ \s -> s
{ tmsModules = modules
, tmsGlobals = globalDefs
}
cmdOut "load-config-done"
cmdRun :: Command
cmdRun = do
[ name ] <- asks tmiParams
TestModeState {..} <- get
case find ((name ==) . testName) $ concatMap moduleTests tmsModules of
Nothing -> cmdOut "run-not-found"
Just test -> do
runSingleTest test >>= \case
True -> cmdOut "run-done"
False -> cmdOut "run-failed"
cmdRunAll :: Command
cmdRunAll = do
TestModeState {..} <- get
forM_ (concatMap moduleTests tmsModules) $ \test -> do
res <- runSingleTest test
cmdOut $ "run-test-result " <> testName test <> " " <> (if res then "done" else "failed")
cmdOut "run-all-done"