summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--erebos-tester.cabal1
-rw-r--r--src/Main.hs12
-rw-r--r--src/Parser.hs80
-rw-r--r--src/Test.hs53
-rw-r--r--src/Util.hs5
5 files changed, 119 insertions, 32 deletions
diff --git a/erebos-tester.cabal b/erebos-tester.cabal
index 2ed105a..ed6398b 100644
--- a/erebos-tester.cabal
+++ b/erebos-tester.cabal
@@ -52,6 +52,7 @@ executable erebos-tester-core
RankNTypes
ScopedTypeVariables
TupleSections
+ TypeApplications
TypeFamilies
TypeOperators
build-depends: base >=4.13 && <5,
diff --git a/src/Main.hs b/src/Main.hs
index 7c6d587..bb5ec02 100644
--- a/src/Main.hs
+++ b/src/Main.hs
@@ -76,7 +76,7 @@ data TestEnv = TestEnv
}
data TestState = TestState
- { tsVars :: [(VarName, Text)]
+ { tsVars :: [(VarName, SomeVarValue)]
}
newtype TestRun a = TestRun { fromTestRun :: ReaderT TestEnv (StateT TestState (ExceptT () IO)) a }
@@ -96,7 +96,7 @@ instance MonadError () TestRun where
catchError (TestRun act) handler = TestRun $ catchError act $ fromTestRun . handler
instance MonadEval TestRun where
- lookupStringVar name = maybe (fail $ "variable not in scope: '" ++ unpackVarName name ++ "'") return =<< gets (lookup name . tsVars)
+ lookupVar name = maybe (fail $ "variable not in scope: '" ++ unpackVarName name ++ "'") return =<< gets (lookup name . tsVars)
instance MonadOutput TestRun where
getOutput = asks teOutput
@@ -203,7 +203,7 @@ getNode net nname@(NodeName tnname) = (find ((nname==).nodeName) <$> liftIO (rea
callOn node $ "ip link set dev lo up"
return $ (node : nodes, ip)
- modify $ \s -> s { tsVars = (VarName [tnname, T.pack "ip"], T.pack ip) : tsVars s }
+ modify $ \s -> s { tsVars = (VarName [tnname, T.pack "ip"], SomeVarValue (T.pack ip)) : tsVars s }
return node
callOn :: Node -> String -> IO ()
@@ -291,13 +291,13 @@ expect (SourceLine sline) p expr vars = do
outLine OutputError (Just $ procName p) $ T.pack "variable '" `T.append` textVarName name `T.append` T.pack "' already exists on " `T.append` sline
throwError ()
- modify $ \s -> s { tsVars = zip vars capture ++ tsVars s }
+ modify $ \s -> s { tsVars = zip vars (map SomeVarValue capture) ++ tsVars s }
outLine OutputMatch (Just $ procName p) line
Nothing -> do
outLine OutputMatchFail (Just $ procName p) $ T.pack "expect failed on " `T.append` sline
exprVars <- gatherVars expr
forM_ exprVars $ \(name, value) ->
- outLine OutputMatchFail (Just $ procName p) $ T.concat [T.pack " ", textVarName name, T.pack " = ", T.pack (show value)]
+ outLine OutputMatchFail (Just $ procName p) $ T.concat [T.pack " ", textVarName name, T.pack " = ", textSomeVarValue value]
throwError ()
testStepGuard :: SourceLine -> Expr Bool -> TestRun ()
@@ -342,7 +342,7 @@ runTest out opts test = do
outLine OutputError Nothing $ T.pack "variable '" `T.append` textVarName name `T.append` T.pack "' already exists on " `T.append` sline
throwError ()
value <- eval expr
- modify $ \s -> s { tsVars = (name, value) : tsVars s }
+ modify $ \s -> s { tsVars = (name, SomeVarValue value) : tsVars s }
Spawn pname nname -> do
node <- getNode net nname
diff --git a/src/Parser.hs b/src/Parser.hs
index bce5a02..760b744 100644
--- a/src/Parser.hs
+++ b/src/Parser.hs
@@ -16,6 +16,7 @@ import Data.Text (Text)
import Data.Text qualified as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.IO as TL
+import Data.Typeable
import Data.Void
import Generics.Deriving.Base as G
@@ -28,12 +29,23 @@ import System.Exit
import Test
-type TestParser = ParsecT Void TestStream (State (Set ProcName))
+type TestParser = ParsecT Void TestStream (State TestParserState)
type TestStream = TL.Text
+data TestParserState = TestParserState
+ { testProcs :: Set ProcName
+ , testVars :: [(VarName, SomeExprType)]
+ }
+
+data SomeExprType = forall a. ExprType a => SomeExprType (Proxy a)
+
+someEmptyVar :: SomeExprType -> SomeVarValue
+someEmptyVar (SomeExprType (Proxy :: Proxy a)) = SomeVarValue $ emptyVarValue @a
+
instance MonadEval TestParser where
- lookupStringVar _ = return T.empty
+ lookupVar (VarName [_, ip]) | ip == T.pack "ip" = return $ SomeVarValue T.empty
+ lookupVar name = maybe (fail $ "variable not in scope: '" ++ unpackVarName name ++ "'") (return . someEmptyVar) =<< gets (lookup name . testVars)
skipLineComment :: TestParser ()
skipLineComment = L.skipLineComment $ TL.pack "#"
@@ -97,6 +109,19 @@ varName = do
VarName . T.splitOn (T.singleton '.') . TL.toStrict <$>
takeWhile1P Nothing (\x -> isAlphaNum x || x == '_' || x == '.')
+newVarName :: forall a proxy. ExprType a => proxy a -> TestParser VarName
+newVarName proxy = do
+ name <- VarName . (:[]) <$> identifier
+ addVarName proxy name
+ return name
+
+addVarName :: forall a proxy. ExprType a => proxy a -> VarName -> TestParser ()
+addVarName _ name = do
+ gets (lookup name . testVars) >>= \case
+ Just _ -> fail $ "variable '" ++ unpackVarName name ++ "' already exists"
+ Nothing -> return ()
+ modify $ \s -> s { testVars = (name, SomeExprType @a Proxy) : testVars s }
+
varExpansion :: TestParser VarName
varExpansion = do
void $ char '$'
@@ -108,12 +133,15 @@ varExpansion = do
return name
]
+integerLiteral :: TestParser (Expr Integer)
+integerLiteral = Literal . read . TL.unpack <$> takeWhile1P (Just "integer") isDigit
+
quotedString :: TestParser (Expr Text)
quotedString = label "string" $ lexeme $ do
symbol "\""
let inner = choice
[ char '"' >> return []
- , takeWhile1P Nothing (`notElem` "\"\\$") >>= \s -> (StringLit (TL.toStrict s):) <$> inner
+ , takeWhile1P Nothing (`notElem` "\"\\$") >>= \s -> (Literal (TL.toStrict s):) <$> inner
,do void $ char '\\'
c <- choice
[ char '\\' >> return '\\'
@@ -123,9 +151,9 @@ quotedString = label "string" $ lexeme $ do
, char 'r' >> return '\r'
, char 't' >> return '\t'
]
- (StringLit (T.singleton c) :) <$> inner
+ (Literal (T.singleton c) :) <$> inner
,do name <- varExpansion
- (StringVar name :) <$> inner
+ (Variable name :) <$> inner
]
Concat <$> inner
@@ -134,24 +162,36 @@ regex = label "regular expression" $ lexeme $ do
symbol "/"
let inner = choice
[ char '/' >> return []
- , takeWhile1P Nothing (`notElem` "/\\$") >>= \s -> (StringLit (TL.toStrict s) :) <$> inner
+ , takeWhile1P Nothing (`notElem` "/\\$") >>= \s -> (Literal (TL.toStrict s) :) <$> inner
,do void $ char '\\'
s <- choice
- [ char '/' >> return (StringLit $ T.singleton '/')
- , anySingle >>= \c -> return (StringLit $ T.pack ['\\', c])
+ [ char '/' >> return (Literal $ T.singleton '/')
+ , anySingle >>= \c -> return (Literal $ T.pack ['\\', c])
]
(s:) <$> inner
,do name <- varExpansion
- (StringVar name :) <$> inner
+ (Variable name :) <$> inner
]
expr <- Regex <$> inner
_ <- eval expr -- test regex parsing with empty variables
return expr
+integerExpr :: TestParser (Expr Integer)
+integerExpr = choice
+ [ integerLiteral
+ , try $ do
+ name <- varName
+ _ <- fromSomeVarValue @Integer name =<< lookupVar name
+ return $ Variable name
+ ]
+
stringExpr :: TestParser (Expr Text)
stringExpr = choice
[ quotedString
- , StringVar <$> varName
+ , try $ do
+ name <- varName
+ _ <- fromSomeVarValue @Text name =<< lookupVar name
+ return $ Variable name
]
boolExpr :: TestParser (Expr Bool)
@@ -192,8 +232,16 @@ letStatement = do
name <- VarName . (:[]) <$> identifier
sc
symbol "="
- value <- stringExpr
- return [Let line name value]
+ let finish :: forall a. ExprType a => TestParser (Expr a) -> TestParser [TestStep]
+ finish expr = do
+ value <- expr
+ addVarName @a Proxy name
+ return [Let line name value]
+
+ choice
+ [ finish integerExpr
+ , finish stringExpr
+ ]
command :: (Generic b, GInit (Rep b)) => String -> [Param b] -> (SourceLine -> b -> TestParser a) -> TestParser [a]
@@ -275,7 +323,7 @@ testExpect :: TestParser [TestStep]
testExpect = command "expect"
[ Param "from" expectBuilderProc procName
, Param "" expectBuilderRegex regex
- , Param "capture" expectBuilderCaptures (listOf $ VarName . (:[]) <$> identifier)
+ , Param "capture" expectBuilderCaptures (listOf $ newVarName @Text Proxy)
] $ \s b -> Expect s
<$> (maybe (fail "missing 'from' <proc>") return $ b ^. expectBuilderProc)
<*> (maybe (fail "missing regex to match") return $ b ^. expectBuilderRegex)
@@ -324,6 +372,10 @@ parseTestDefinitions = do
parseTestFile :: FilePath -> IO [Test]
parseTestFile path = do
content <- TL.readFile path
- case evalState (runParserT parseTestDefinitions path content) S.empty of
+ let initState = TestParserState
+ { testProcs = S.empty
+ , testVars = []
+ }
+ case evalState (runParserT parseTestDefinitions path content) initState of
Left err -> putStr (errorBundlePretty err) >> exitFailure
Right tests -> return tests
diff --git a/src/Test.hs b/src/Test.hs
index 80ee966..16c1b1f 100644
--- a/src/Test.hs
+++ b/src/Test.hs
@@ -8,6 +8,8 @@ module Test (
MonadEval(..),
VarName(..), textVarName, unpackVarName,
+ ExprType(..),
+ SomeVarValue(..), fromSomeVarValue, textSomeVarValue,
Expr(..), eval, gatherVars,
Regex,
) where
@@ -18,6 +20,7 @@ import Data.Char
import Data.List
import Data.Text (Text)
import qualified Data.Text as T
+import Data.Typeable
import Text.Regex.TDFA
import Text.Regex.TDFA.Text
@@ -30,7 +33,7 @@ data Test = Test
, testSteps :: [TestStep]
}
-data TestStep = Let SourceLine VarName (Expr Text)
+data TestStep = forall a. ExprType a => Let SourceLine VarName (Expr a)
| Spawn ProcName NodeName
| Send ProcName (Expr Text)
| Expect SourceLine ProcName (Expr Regex) [VarName]
@@ -50,7 +53,7 @@ unpackNodeName (NodeName tname) = T.unpack tname
class MonadFail m => MonadEval m where
- lookupStringVar :: VarName -> m Text
+ lookupVar :: VarName -> m SomeVarValue
data VarName = VarName [Text]
@@ -63,20 +66,46 @@ unpackVarName :: VarName -> String
unpackVarName = T.unpack . textVarName
+class Typeable a => ExprType a where
+ textExprType :: proxy a -> Text
+ textExprValue :: a -> Text
+ emptyVarValue :: a
+
+instance ExprType Integer where
+ textExprType _ = T.pack "integer"
+ textExprValue x = T.pack (show x)
+ emptyVarValue = 0
+
+instance ExprType Text where
+ textExprType _ = T.pack "string"
+ textExprValue x = T.pack (show x)
+ emptyVarValue = T.empty
+
+data SomeVarValue = forall a. ExprType a => SomeVarValue a
+
+fromSomeVarValue :: forall a m. (ExprType a, MonadFail m) => VarName -> SomeVarValue -> m a
+fromSomeVarValue name (SomeVarValue value) = maybe (fail err) return $ cast value
+ where err = T.unpack $ T.concat [ T.pack "expected ", textExprType @a Proxy, T.pack ", but variable '", textVarName name, T.pack "' has type ", textExprType (Just value) ]
+
+textSomeVarValue :: SomeVarValue -> Text
+textSomeVarValue (SomeVarValue value) = textExprValue value
+
+
data Expr a where
- StringVar :: VarName -> Expr Text
- StringLit :: Text -> Expr Text
+ Variable :: ExprType a => VarName -> Expr a
+ Literal :: ExprType a => a -> Expr a
Concat :: [Expr Text] -> Expr Text
Regex :: [Expr Text] -> Expr Regex
BinOp :: (b -> c -> a) -> Expr b -> Expr c -> Expr a
eval :: MonadEval m => Expr a -> m a
-eval (StringVar var) = lookupStringVar var
-eval (StringLit str) = return str
+eval (Variable name) = fromSomeVarValue name =<< lookupVar name
+eval (Literal value) = return value
eval (Concat xs) = T.concat <$> mapM eval xs
eval (Regex xs) = do
parts <- forM xs $ \case
- StringLit str -> return str
+ Literal value | Just str <- cast value -> return str
+ | otherwise -> fail $ "regex expansion not defined for " ++ T.unpack (textExprType $ Just value)
expr -> T.concatMap escapeChar <$> eval expr
where
escapeChar c | isAlphaNum c = T.singleton c
@@ -87,12 +116,12 @@ eval (Regex xs) = do
Right re -> return re
eval (BinOp f x y) = f <$> eval x <*> eval y
-gatherVars :: forall a m. MonadEval m => Expr a -> m [(VarName, Text)]
-gatherVars = fmap (uniq . sort) . helper
+gatherVars :: forall a m. MonadEval m => Expr a -> m [(VarName, SomeVarValue)]
+gatherVars = fmap (uniqOn fst . sortOn fst) . helper
where
- helper :: forall b. Expr b -> m [(VarName, Text)]
- helper (StringVar var) = (:[]) . (var,) <$> lookupStringVar var
- helper (StringLit _) = return []
+ helper :: forall b. Expr b -> m [(VarName, SomeVarValue)]
+ helper (Variable var) = (:[]) . (var,) <$> lookupVar var
+ helper (Literal _) = return []
helper (Concat es) = concat <$> mapM helper es
helper (Regex es) = concat <$> mapM helper es
helper (BinOp _ e f) = (++) <$> helper e <*> helper f
diff --git a/src/Util.hs b/src/Util.hs
index 99d51f6..4200e20 100644
--- a/src/Util.hs
+++ b/src/Util.hs
@@ -4,3 +4,8 @@ uniq :: Eq a => [a] -> [a]
uniq (x:y:xs) | x == y = uniq (x:xs)
| otherwise = x : uniq (y:xs)
uniq xs = xs
+
+uniqOn :: Eq b => (a -> b) -> [a] -> [a]
+uniqOn f (x:y:xs) | f x == f y = uniqOn f (x:xs)
+ | otherwise = x : uniqOn f (y:xs)
+uniqOn _ xs = xs