diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/Main.hs | 249 | ||||
-rw-r--r-- | src/Run.hs | 264 |
2 files changed, 265 insertions, 248 deletions
diff --git a/src/Main.hs b/src/Main.hs index b49fe09..c9fbf50 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -1,16 +1,8 @@ module Main (main) where -import Control.Applicative -import Control.Concurrent -import Control.Concurrent.STM import Control.Monad -import Control.Monad.Except -import Control.Monad.Reader -import Data.Map qualified as M import Data.Maybe -import Data.Scientific -import Data.Text (Text) import qualified Data.Text as T import Text.Read (readMaybe) @@ -21,254 +13,15 @@ import System.Environment import System.Exit import System.FilePath import System.FilePath.Glob -import System.IO.Error -import System.Posix.Process -import System.Posix.Signals -import System.Process import Config -import GDB -import Network import Output import Parser import Process -import Run.Monad -import Test +import Run import Util import Version -withVar :: ExprType e => VarName -> e -> TestRun a -> TestRun a -withVar name value = local (fmap $ \s -> s { tsVars = (name, SomeVarValue value) : tsVars s }) - -withNodePacketLoss :: Node -> Scientific -> TestRun a -> TestRun a -withNodePacketLoss node loss inner = do - x <- local (fmap $ \s -> s { tsNodePacketLoss = M.insertWith (\l l' -> 1 - (1 - l) * (1 - l')) (nodeName node) loss $ tsNodePacketLoss s }) $ do - resetLoss - inner - resetLoss - return x - where - resetLoss = do - tl <- asks $ fromMaybe 0 . M.lookup (nodeName node) . tsNodePacketLoss . snd - liftIO $ callOn node $ "tc qdisc replace dev veth0 root netem loss " <> T.pack (show (tl * 100)) <> "%" - liftIO $ putStrLn $ "tc qdisc replace dev veth0 root netem loss " <> show (tl * 100) <> "%" - -atomicallyTest :: STM a -> TestRun a -atomicallyTest act = do - failedVar <- asks $ teFailed . fst - res <- liftIO $ atomically $ do - readTVar failedVar >>= \case - Just e -> return $ Left e - Nothing -> Right <$> act - case res of - Left e -> throwError e - Right x -> return x - -withInternet :: (Network -> TestRun a) -> TestRun a -withInternet inner = do - testDir <- asks $ optTestDir . teOptions . fst - inet <- newInternet testDir - res <- withNetwork (inetRoot inet) $ \net -> do - local (fmap $ \s -> s { tsNetwork = net }) $ inner net - delInternet inet - return res - -withSubnet :: Network -> Maybe (TypedVarName Network) -> (Network -> TestRun a) -> TestRun a -withSubnet parent tvname inner = do - net <- newSubnet parent (fromTypedVarName <$> tvname) - withNetwork net inner - -withNetwork :: Network -> (Network -> TestRun a) -> TestRun a -withNetwork net inner = do - tcpdump <- liftIO (findExecutable "tcpdump") >>= return . \case - Just path -> withProcess (Left net) ProcNameTcpdump (Just softwareTermination) - (path ++ " -i br0 -w '" ++ netDir net ++ "/br0.pcap' -U -Z root") . const - Nothing -> id - - tcpdump $ inner net - -withNode :: Expr Network -> Either (TypedVarName Node) (TypedVarName Process) -> (Node -> TestRun a) -> TestRun a -withNode netexpr tvname inner = do - net <- eval netexpr - node <- newNode net (either fromTypedVarName fromTypedVarName tvname) - either (flip withVar node . fromTypedVarName) (const id) tvname $ inner node - -tryMatch :: Regex -> [Text] -> Maybe ((Text, [Text]), [Text]) -tryMatch re (x:xs) | Right (Just (_, _, _, capture)) <- regexMatch re x = Just ((x, capture), xs) - | otherwise = fmap (x:) <$> tryMatch re xs -tryMatch _ [] = Nothing - -exprFailed :: Text -> SourceLine -> Maybe ProcName -> Expr a -> TestRun () -exprFailed desc (SourceLine sline) pname expr = do - let prompt = maybe T.empty textProcName pname - exprVars <- gatherVars expr - outLine OutputMatchFail (Just prompt) $ T.concat [desc, T.pack " failed on ", sline] - forM_ exprVars $ \(name, value) -> - outLine OutputMatchFail (Just prompt) $ T.concat [T.pack " ", textVarName name, T.pack " = ", textSomeVarValue value] - throwError Failed - -expect :: SourceLine -> Process -> Expr Regex -> [TypedVarName Text] -> TestRun () -> TestRun () -expect (SourceLine sline) p expr tvars inner = do - re <- eval expr - timeout <- asks $ optTimeout . teOptions . fst - delay <- liftIO $ registerDelay $ ceiling $ 1000000 * timeout - mbmatch <- atomicallyTest $ (Nothing <$ (check =<< readTVar delay)) <|> do - line <- readTVar (procOutput p) - case tryMatch re line of - Nothing -> retry - Just (m, out') -> do - writeTVar (procOutput p) out' - return $ Just m - case mbmatch of - Just (line, capture) -> do - let vars = map (\(TypedVarName n) -> n) tvars - - when (length vars /= length capture) $ do - outProc OutputMatchFail p $ T.pack "mismatched number of capture variables on " `T.append` sline - throwError Failed - - forM_ vars $ \name -> do - cur <- asks (lookup name . tsVars . snd) - when (isJust cur) $ do - outProc OutputError p $ T.pack "variable '" `T.append` textVarName name `T.append` T.pack "' already exists on " `T.append` sline - throwError Failed - - outProc OutputMatch p line - local (fmap $ \s -> s { tsVars = zip vars (map SomeVarValue capture) ++ tsVars s }) inner - - Nothing -> exprFailed (T.pack "expect") (SourceLine sline) (Just $ procName p) expr - -testStepGuard :: SourceLine -> Expr Bool -> TestRun () -testStepGuard sline expr = do - x <- eval expr - when (not x) $ exprFailed (T.pack "guard") sline Nothing expr - -evalSteps :: [TestStep] -> TestRun () -evalSteps = mapM_ $ \case - Let (SourceLine sline) (TypedVarName name) expr inner -> do - cur <- asks (lookup name . tsVars . snd) - when (isJust cur) $ do - outLine OutputError Nothing $ T.pack "variable '" `T.append` textVarName name `T.append` T.pack "' already exists on " `T.append` sline - throwError Failed - value <- eval expr - withVar name value $ evalSteps inner - - For (SourceLine sline) (TypedVarName name) expr inner -> do - cur <- asks (lookup name . tsVars . snd) - when (isJust cur) $ do - outLine OutputError Nothing $ T.pack "variable '" `T.append` textVarName name `T.append` T.pack "' already exists on " `T.append` sline - throwError Failed - value <- eval expr - forM_ value $ \i -> do - withVar name i $ evalSteps inner - - Subnet name@(TypedVarName vname) parentExpr inner -> do - parent <- eval parentExpr - withSubnet parent (Just name) $ \net -> do - withVar vname net $ evalSteps inner - - DeclNode name@(TypedVarName vname) net inner -> do - withNode net (Left name) $ \node -> do - withVar vname node $ evalSteps inner - - Spawn tvname@(TypedVarName vname@(VarName tname)) target inner -> do - case target of - Left nname -> withNode RootNetwork (Left nname) go - Right (Left net) -> withNode net (Right tvname) go - Right (Right node) -> go =<< eval node - where - go node = do - opts <- asks $ teOptions . fst - let pname = ProcName tname - tool = fromMaybe (optDefaultTool opts) (lookup pname $ optProcTools opts) - withProcess (Right node) pname Nothing tool $ \p -> do - withVar vname p (evalSteps inner) - - Send pname expr -> do - p <- eval pname - line <- eval expr - outProc OutputChildStdin p line - send p line - - Expect line pname expr captures inner -> do - p <- eval pname - expect line p expr captures $ evalSteps inner - - Guard line expr -> do - testStepGuard line expr - - PacketLoss loss node inner -> do - l <- eval loss - n <- eval node - withNodePacketLoss n l $ evalSteps inner - - Wait -> do - void $ outPromptGetLine "Waiting..." - -runTest :: Output -> TestOptions -> Test -> IO Bool -runTest out opts test = do - let testDir = optTestDir opts - when (optForce opts) $ removeDirectoryRecursive testDir `catchIOError` \e -> - if isDoesNotExistError e then return () else ioError e - exists <- doesPathExist testDir - when exists $ ioError $ userError $ testDir ++ " exists" - createDirectoryIfMissing True testDir - - failedVar <- newTVarIO Nothing - procVar <- newMVar [] - - mgdb <- if optGDB opts - then flip runReaderT out $ do - gdb <- gdbStart $ atomically . writeTVar failedVar . Just . ProcessCrashed - Just . (, gdbProcess gdb) <$> liftIO (newMVar gdb) - else return Nothing - - let tenv = TestEnv - { teOutput = out - , teFailed = failedVar - , teOptions = opts - , teProcesses = procVar - , teGDB = fst <$> mgdb - } - tstate = TestState - { tsNetwork = error "network not initialized" - , tsVars = [] - , tsNodePacketLoss = M.empty - } - - let sigHandler SignalInfo { siginfoSpecific = chld } = do - processes <- readMVar procVar - forM_ processes $ \p -> do - mbpid <- getPid (procHandle p) - when (mbpid == Just (siginfoPid chld)) $ flip runReaderT out $ do - let err detail = outProc OutputChildFail p detail - case siginfoStatus chld of - Exited ExitSuccess -> outProc OutputChildInfo p $ T.pack $ "child exited successfully" - Exited (ExitFailure code) -> do - err $ T.pack $ "child process exited with status " ++ show code - liftIO $ atomically $ writeTVar (teFailed tenv) $ Just Failed - Terminated sig _ -> do - err $ T.pack $ "child terminated with signal " ++ show sig - liftIO $ atomically $ writeTVar (teFailed tenv) $ Just $ ProcessCrashed p - Stopped sig -> err $ T.pack $ "child stopped with signal " ++ show sig - oldHandler <- installHandler processStatusChanged (CatchInfo sigHandler) Nothing - - res <- runExceptT $ flip runReaderT (tenv, tstate) $ fromTestRun $ do - withInternet $ \_ -> evalSteps (testSteps test) - - void $ installHandler processStatusChanged oldHandler Nothing - - Right () <- runExceptT $ flip runReaderT out $ do - maybe (return ()) (closeProcess . snd) mgdb - [] <- readMVar procVar - - failed <- atomically $ readTVar (teFailed tenv) - case (res, failed) of - (Right (), Nothing) -> do - removeDirectoryRecursive testDir - return True - _ -> return False - data CmdlineOptions = CmdlineOptions { optTest :: TestOptions , optShowVersion :: Bool diff --git a/src/Run.hs b/src/Run.hs new file mode 100644 index 0000000..d771116 --- /dev/null +++ b/src/Run.hs @@ -0,0 +1,264 @@ +module Run ( + module Run.Monad, + runTest, +) where + +import Control.Applicative +import Control.Concurrent +import Control.Concurrent.STM +import Control.Monad +import Control.Monad.Except +import Control.Monad.Reader + +import Data.Map qualified as M +import Data.Maybe +import Data.Scientific +import Data.Text (Text) +import qualified Data.Text as T + +import System.Directory +import System.Exit +import System.IO.Error +import System.Posix.Process +import System.Posix.Signals +import System.Process + +import GDB +import Network +import Output +import Process +import Run.Monad +import Test + +runTest :: Output -> TestOptions -> Test -> IO Bool +runTest out opts test = do + let testDir = optTestDir opts + when (optForce opts) $ removeDirectoryRecursive testDir `catchIOError` \e -> + if isDoesNotExistError e then return () else ioError e + exists <- doesPathExist testDir + when exists $ ioError $ userError $ testDir ++ " exists" + createDirectoryIfMissing True testDir + + failedVar <- newTVarIO Nothing + procVar <- newMVar [] + + mgdb <- if optGDB opts + then flip runReaderT out $ do + gdb <- gdbStart $ atomically . writeTVar failedVar . Just . ProcessCrashed + Just . (, gdbProcess gdb) <$> liftIO (newMVar gdb) + else return Nothing + + let tenv = TestEnv + { teOutput = out + , teFailed = failedVar + , teOptions = opts + , teProcesses = procVar + , teGDB = fst <$> mgdb + } + tstate = TestState + { tsNetwork = error "network not initialized" + , tsVars = [] + , tsNodePacketLoss = M.empty + } + + let sigHandler SignalInfo { siginfoSpecific = chld } = do + processes <- readMVar procVar + forM_ processes $ \p -> do + mbpid <- getPid (procHandle p) + when (mbpid == Just (siginfoPid chld)) $ flip runReaderT out $ do + let err detail = outProc OutputChildFail p detail + case siginfoStatus chld of + Exited ExitSuccess -> outProc OutputChildInfo p $ T.pack $ "child exited successfully" + Exited (ExitFailure code) -> do + err $ T.pack $ "child process exited with status " ++ show code + liftIO $ atomically $ writeTVar (teFailed tenv) $ Just Failed + Terminated sig _ -> do + err $ T.pack $ "child terminated with signal " ++ show sig + liftIO $ atomically $ writeTVar (teFailed tenv) $ Just $ ProcessCrashed p + Stopped sig -> err $ T.pack $ "child stopped with signal " ++ show sig + oldHandler <- installHandler processStatusChanged (CatchInfo sigHandler) Nothing + + res <- runExceptT $ flip runReaderT (tenv, tstate) $ fromTestRun $ do + withInternet $ \_ -> evalSteps (testSteps test) + + void $ installHandler processStatusChanged oldHandler Nothing + + Right () <- runExceptT $ flip runReaderT out $ do + maybe (return ()) (closeProcess . snd) mgdb + [] <- readMVar procVar + + failed <- atomically $ readTVar (teFailed tenv) + case (res, failed) of + (Right (), Nothing) -> do + removeDirectoryRecursive testDir + return True + _ -> return False + +evalSteps :: [TestStep] -> TestRun () +evalSteps = mapM_ $ \case + Let (SourceLine sline) (TypedVarName name) expr inner -> do + cur <- asks (lookup name . tsVars . snd) + when (isJust cur) $ do + outLine OutputError Nothing $ T.pack "variable '" `T.append` textVarName name `T.append` T.pack "' already exists on " `T.append` sline + throwError Failed + value <- eval expr + withVar name value $ evalSteps inner + + For (SourceLine sline) (TypedVarName name) expr inner -> do + cur <- asks (lookup name . tsVars . snd) + when (isJust cur) $ do + outLine OutputError Nothing $ T.pack "variable '" `T.append` textVarName name `T.append` T.pack "' already exists on " `T.append` sline + throwError Failed + value <- eval expr + forM_ value $ \i -> do + withVar name i $ evalSteps inner + + Subnet name@(TypedVarName vname) parentExpr inner -> do + parent <- eval parentExpr + withSubnet parent (Just name) $ \net -> do + withVar vname net $ evalSteps inner + + DeclNode name@(TypedVarName vname) net inner -> do + withNode net (Left name) $ \node -> do + withVar vname node $ evalSteps inner + + Spawn tvname@(TypedVarName vname@(VarName tname)) target inner -> do + case target of + Left nname -> withNode RootNetwork (Left nname) go + Right (Left net) -> withNode net (Right tvname) go + Right (Right node) -> go =<< eval node + where + go node = do + opts <- asks $ teOptions . fst + let pname = ProcName tname + tool = fromMaybe (optDefaultTool opts) (lookup pname $ optProcTools opts) + withProcess (Right node) pname Nothing tool $ \p -> do + withVar vname p (evalSteps inner) + + Send pname expr -> do + p <- eval pname + line <- eval expr + outProc OutputChildStdin p line + send p line + + Expect line pname expr captures inner -> do + p <- eval pname + expect line p expr captures $ evalSteps inner + + Guard line expr -> do + testStepGuard line expr + + PacketLoss loss node inner -> do + l <- eval loss + n <- eval node + withNodePacketLoss n l $ evalSteps inner + + Wait -> do + void $ outPromptGetLine "Waiting..." + + +withVar :: ExprType e => VarName -> e -> TestRun a -> TestRun a +withVar name value = local (fmap $ \s -> s { tsVars = (name, SomeVarValue value) : tsVars s }) + +withInternet :: (Network -> TestRun a) -> TestRun a +withInternet inner = do + testDir <- asks $ optTestDir . teOptions . fst + inet <- newInternet testDir + res <- withNetwork (inetRoot inet) $ \net -> do + local (fmap $ \s -> s { tsNetwork = net }) $ inner net + delInternet inet + return res + +withSubnet :: Network -> Maybe (TypedVarName Network) -> (Network -> TestRun a) -> TestRun a +withSubnet parent tvname inner = do + net <- newSubnet parent (fromTypedVarName <$> tvname) + withNetwork net inner + +withNetwork :: Network -> (Network -> TestRun a) -> TestRun a +withNetwork net inner = do + tcpdump <- liftIO (findExecutable "tcpdump") >>= return . \case + Just path -> withProcess (Left net) ProcNameTcpdump (Just softwareTermination) + (path ++ " -i br0 -w '" ++ netDir net ++ "/br0.pcap' -U -Z root") . const + Nothing -> id + + tcpdump $ inner net + +withNode :: Expr Network -> Either (TypedVarName Node) (TypedVarName Process) -> (Node -> TestRun a) -> TestRun a +withNode netexpr tvname inner = do + net <- eval netexpr + node <- newNode net (either fromTypedVarName fromTypedVarName tvname) + either (flip withVar node . fromTypedVarName) (const id) tvname $ inner node + +withNodePacketLoss :: Node -> Scientific -> TestRun a -> TestRun a +withNodePacketLoss node loss inner = do + x <- local (fmap $ \s -> s { tsNodePacketLoss = M.insertWith (\l l' -> 1 - (1 - l) * (1 - l')) (nodeName node) loss $ tsNodePacketLoss s }) $ do + resetLoss + inner + resetLoss + return x + where + resetLoss = do + tl <- asks $ fromMaybe 0 . M.lookup (nodeName node) . tsNodePacketLoss . snd + liftIO $ callOn node $ "tc qdisc replace dev veth0 root netem loss " <> T.pack (show (tl * 100)) <> "%" + + +atomicallyTest :: STM a -> TestRun a +atomicallyTest act = do + failedVar <- asks $ teFailed . fst + res <- liftIO $ atomically $ do + readTVar failedVar >>= \case + Just e -> return $ Left e + Nothing -> Right <$> act + case res of + Left e -> throwError e + Right x -> return x + +tryMatch :: Regex -> [Text] -> Maybe ((Text, [Text]), [Text]) +tryMatch re (x:xs) | Right (Just (_, _, _, capture)) <- regexMatch re x = Just ((x, capture), xs) + | otherwise = fmap (x:) <$> tryMatch re xs +tryMatch _ [] = Nothing + +exprFailed :: Text -> SourceLine -> Maybe ProcName -> Expr a -> TestRun () +exprFailed desc (SourceLine sline) pname expr = do + let prompt = maybe T.empty textProcName pname + exprVars <- gatherVars expr + outLine OutputMatchFail (Just prompt) $ T.concat [desc, T.pack " failed on ", sline] + forM_ exprVars $ \(name, value) -> + outLine OutputMatchFail (Just prompt) $ T.concat [T.pack " ", textVarName name, T.pack " = ", textSomeVarValue value] + throwError Failed + +expect :: SourceLine -> Process -> Expr Regex -> [TypedVarName Text] -> TestRun () -> TestRun () +expect (SourceLine sline) p expr tvars inner = do + re <- eval expr + timeout <- asks $ optTimeout . teOptions . fst + delay <- liftIO $ registerDelay $ ceiling $ 1000000 * timeout + mbmatch <- atomicallyTest $ (Nothing <$ (check =<< readTVar delay)) <|> do + line <- readTVar (procOutput p) + case tryMatch re line of + Nothing -> retry + Just (m, out') -> do + writeTVar (procOutput p) out' + return $ Just m + case mbmatch of + Just (line, capture) -> do + let vars = map (\(TypedVarName n) -> n) tvars + + when (length vars /= length capture) $ do + outProc OutputMatchFail p $ T.pack "mismatched number of capture variables on " `T.append` sline + throwError Failed + + forM_ vars $ \name -> do + cur <- asks (lookup name . tsVars . snd) + when (isJust cur) $ do + outProc OutputError p $ T.pack "variable '" `T.append` textVarName name `T.append` T.pack "' already exists on " `T.append` sline + throwError Failed + + outProc OutputMatch p line + local (fmap $ \s -> s { tsVars = zip vars (map SomeVarValue capture) ++ tsVars s }) inner + + Nothing -> exprFailed (T.pack "expect") (SourceLine sline) (Just $ procName p) expr + +testStepGuard :: SourceLine -> Expr Bool -> TestRun () +testStepGuard sline expr = do + x <- eval expr + when (not x) $ exprFailed (T.pack "guard") sline Nothing expr |