ribosome-test-0.3.0.0: lib/Ribosome/Test/Embed.hs
module Ribosome.Test.Embed where
import Control.Concurrent (forkIO)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Reader (runReaderT)
import Control.Monad.Trans.Except (runExceptT)
import Control.Monad.Trans.Resource (runResourceT)
import Data.Default (Default(def))
import Data.Foldable (traverse_)
import Data.Functor (void)
import Data.Maybe (fromMaybe)
import GHC.IO.Handle (Handle)
import Neovim (Neovim, Object)
import Neovim.API.Text (vim_command)
import qualified Neovim.Context.Internal as Internal (
Config,
Neovim(Neovim),
StateTransition(Failure, InitSuccess, Quit),
globalFunctionMap,
mkFunctionMap,
newConfig,
pluginSettings,
retypeConfig,
transitionTo,
)
import Neovim.Main (standalone)
import Neovim.Plugin (Plugin, startPluginThreads)
import Neovim.Plugin.Internal (NeovimPlugin, wrapPlugin)
import Neovim.RPC.Common (RPCConfig, newRPCConfig)
import Neovim.RPC.EventHandler (runEventHandler)
import Neovim.RPC.SocketReader (runSocketReader)
import System.Directory (makeAbsolute)
import System.Exit (ExitCode)
import System.Log.Logger (Priority(ERROR), setLevel, updateGlobalLogger)
import qualified System.Posix.Signals as Signal (killProcess, signalProcess)
import System.Process (getPid)
import System.Process.Typed (
Process,
ProcessConfig,
createPipe,
getExitCode,
getStdin,
getStdout,
proc,
setStdin,
setStdout,
unsafeProcessHandle,
withProcess,
)
import UnliftIO.Async (async, cancel, race)
import UnliftIO.Exception (bracket, tryAny)
import UnliftIO.MVar (MVar)
import UnliftIO.STM (putTMVar)
import Ribosome.Api.Option (rtpCat)
import Ribosome.Control.Monad.Ribo (NvimE)
import Ribosome.Control.Ribosome (Ribosome(Ribosome), newRibosomeTMVar)
import qualified Ribosome.Data.ErrorReport as ErrorReport (ErrorReport(..))
import Ribosome.Error.Report.Class (ReportError(errorReport))
import Ribosome.Nvim.Api.IO (vimSetVar)
import Ribosome.Plugin.RpcHandler (RpcHandler(native))
import Ribosome.System.Time (sleep, sleepW)
import Ribosome.Test.Orphans ()
type Runner m = TestConfig -> m () -> m ()
newtype Vars =
Vars [(Text, Object)]
deriving (Eq, Show, Semigroup, Monoid, Default)
data TestConfig =
TestConfig {
tcPluginName :: Text,
tcExtraRtp :: Text,
tcLogPath :: FilePath,
tcTimeout :: Word,
tcCmdline :: Maybe [Text],
tcCmdArgs :: [Text],
tcVariables :: Vars
}
instance Default TestConfig where
def = TestConfig "ribosome" "test/f/fixtures/rtp" "test/f/temp/log" 10 def def (Vars [])
defaultTestConfigWith :: Text -> Vars -> TestConfig
defaultTestConfigWith name vars =
def { tcPluginName = name, tcVariables = vars }
defaultTestConfig :: Text -> TestConfig
defaultTestConfig name = defaultTestConfigWith name (Vars [])
setVars :: ∀ m e. NvimE e m => Vars -> m ()
setVars (Vars vars) =
traverse_ set vars
where
set :: (Text, Object) -> m ()
set = uncurry vimSetVar
setupPluginEnv :: (MonadIO m, NvimE e m) => TestConfig -> m ()
setupPluginEnv (TestConfig _ rtp _ _ _ _ vars) = do
absRtp <- liftIO $ makeAbsolute (toString rtp)
rtpCat (toText absRtp)
setVars vars
killPid :: Integral a => a -> IO ()
killPid =
void . tryAny . Signal.signalProcess Signal.killProcess . fromIntegral
killProcess :: Process i o e -> IO ()
killProcess prc = do
let handle = unsafeProcessHandle prc
mayPid <- getPid handle
traverse_ killPid mayPid
testNvimProcessConfig :: TestConfig -> ProcessConfig Handle Handle ()
testNvimProcessConfig TestConfig {..} =
setStdin createPipe . setStdout createPipe . proc "nvim" . fmap toString $ args <> tcCmdArgs
where
args = fromMaybe defaultArgs tcCmdline
defaultArgs = ["--embed", "-n", "-u", "NONE", "-i", "NONE"]
startHandlers :: Handle -> Handle -> TestConfig -> Internal.Config RPCConfig -> IO (IO ())
startHandlers stdoutHandle stdinHandle TestConfig{..} nvimConf = do
socketReader <- run runSocketReader stdoutHandle
eventHandler <- run runEventHandler stdinHandle
atomically $ putTMVar (Internal.globalFunctionMap nvimConf) (Internal.mkFunctionMap [])
let stopEventHandlers = traverse_ cancel [socketReader, eventHandler]
return stopEventHandlers
where
run runner hand = async . void $ runner hand emptyConf
emptyConf = nvimConf { Internal.pluginSettings = Nothing }
startStdioHandlers :: NvimProc -> TestConfig -> Internal.Config RPCConfig -> IO (IO ())
startStdioHandlers prc =
startHandlers (getStdout prc) (getStdin prc)
runNeovimThunk :: Internal.Config e -> Neovim e a -> IO a
runNeovimThunk cfg (Internal.Neovim thunk) =
runReaderT (runResourceT thunk) cfg
type NvimProc = Process Handle Handle ()
waitQuit :: NvimProc -> IO (Maybe ExitCode)
waitQuit prc =
wait 30
where
wait :: Int -> IO (Maybe ExitCode)
wait 0 = return Nothing
wait count = do
code <- getExitCode prc
case code of
Just a -> return $ Just a
Nothing -> do
sleep 0.1
wait $ count - 1
quitNvim :: Internal.Config e -> NvimProc -> IO ()
quitNvim testCfg prc = do
quitThread <- async $ runNeovimThunk testCfg quit
result <- waitQuit prc
case result of
Just _ -> return ()
Nothing -> killProcess prc
cancel quitThread
where
quit = vim_command "qall!"
shutdownNvim :: Internal.Config e -> NvimProc -> IO () -> IO ()
shutdownNvim _ prc stopEventHandlers = do
stopEventHandlers
killProcess prc
-- quitNvim testCfg prc
runTest ::
RpcHandler e env m =>
ReportError e =>
TestConfig ->
Internal.Config env ->
m () ->
IO ()
runTest TestConfig{..} testCfg thunk = do
result <- race (sleepW tcTimeout) (runNeovimThunk testCfg (runExceptT $ native thunk))
case result of
Right (Right _) -> return ()
Right (Left e) -> fail . toString . unlines . ErrorReport._log . errorReport $ e
Left _ -> fail $ "test exceeded timeout of " <> show tcTimeout <> " seconds"
runEmbeddedNvim ::
RpcHandler e env m =>
ReportError e =>
TestConfig ->
env ->
m () ->
NvimProc ->
IO ()
runEmbeddedNvim conf ribo thunk prc = do
nvimConf <- Internal.newConfig (pure Nothing) newRPCConfig
let testCfg = Internal.retypeConfig ribo nvimConf
bracket (startStdioHandlers prc conf nvimConf) (shutdownNvim testCfg prc) (const $ runTest conf testCfg thunk)
runEmbedded ::
RpcHandler e env m =>
ReportError e =>
TestConfig ->
env ->
m () ->
IO ()
runEmbedded conf ribo thunk = do
let pc = testNvimProcessConfig conf
withProcess pc $ runEmbeddedNvim conf ribo thunk
unsafeEmbeddedSpec ::
RpcHandler e env m =>
ReportError e =>
Runner m ->
TestConfig ->
env ->
m () ->
IO ()
unsafeEmbeddedSpec runner conf s spec =
runEmbedded conf s $ runner conf spec
unsafeEmbeddedSpecR ::
RpcHandler e (Ribosome env) m =>
ReportError e =>
Runner m ->
TestConfig ->
env ->
m () ->
IO ()
unsafeEmbeddedSpecR runner conf env spec = do
tv <- newRibosomeTMVar env
let ribo = Ribosome (tcPluginName conf) tv
unsafeEmbeddedSpec runner conf ribo spec
runPlugin ::
Handle ->
Handle ->
[Neovim () NeovimPlugin] ->
Internal.Config c ->
IO (MVar Internal.StateTransition)
runPlugin evHandlerHandle sockreaderHandle plugins baseConf = do
updateGlobalLogger "Neovim.Plugin" (setLevel ERROR)
rpcConf <- newRPCConfig
let conf = Internal.retypeConfig rpcConf baseConf
ehTid <- async $ runEventHandler evHandlerHandle conf { Internal.pluginSettings = Nothing }
srTid <- async $ runSocketReader sockreaderHandle conf
void $ forkIO $ startPluginThreads (Internal.retypeConfig () baseConf) plugins >>= \case
Left e -> do
putMVar (Internal.transitionTo conf) $ Internal.Failure e
standalone [ehTid, srTid] conf
Right (funMapEntries, pluginTids) -> do
atomically $ putTMVar (Internal.globalFunctionMap conf) (Internal.mkFunctionMap funMapEntries)
putMVar (Internal.transitionTo conf) Internal.InitSuccess
standalone (srTid:ehTid:pluginTids) conf
return (Internal.transitionTo conf)
runEmbeddedWithPlugin ::
RpcHandler e () m =>
ReportError e =>
TestConfig ->
Plugin env ->
m () ->
IO ()
runEmbeddedWithPlugin conf plugin thunk =
withProcess (testNvimProcessConfig conf) run
where
run prc = do
nvimConf <- Internal.newConfig (pure Nothing) (pure ())
bracket (acquire prc nvimConf) release (const $ runTest conf nvimConf thunk)
acquire prc nvimConf =
runPlugin (getStdin prc) (getStdout prc) [wrapPlugin plugin] nvimConf <* sleep 0.5
release transitions =
tryPutMVar transitions Internal.Quit *> sleep 0.5
integrationSpecDef ::
RpcHandler e () m =>
ReportError e =>
Plugin env ->
m () ->
IO ()
integrationSpecDef =
runEmbeddedWithPlugin def